mirror of
https://github.com/nushell/nushell.git
synced 2025-07-04 16:40:47 +02:00
Compare commits
262 Commits
back-to-th
...
0.101.0
Author | SHA1 | Date | |
---|---|---|---|
fb26109049 | |||
d99905b604 | |||
a8890d5cca | |||
5139054325 | |||
039d0a685a | |||
e0685315b4 | |||
02fc844e40 | |||
b48f50f018 | |||
dc0ac8e917 | |||
f2e8c391a2 | |||
7029d24f42 | |||
4e8289d7bb | |||
bf8763fc11 | |||
11375c19d2 | |||
8f4feeb119 | |||
e26364f885 | |||
fff0c6e2cb | |||
68c2729991 | |||
8127b5dd24 | |||
a9caa61ef9 | |||
99fe866d12 | |||
c0ad659985 | |||
f41c53fef1 | |||
981a000ee8 | |||
cc4da104e0 | |||
c266e6adaf | |||
d94b344342 | |||
6367fb6e9e | |||
5615d21ce9 | |||
e2c4ff8180 | |||
39770d4197 | |||
cfdb4bbf25 | |||
3760910f0b | |||
3c632e96f9 | |||
baf86dfb0e | |||
219b44a04f | |||
05ee7ea9c7 | |||
cc0616b753 | |||
cbf5fa6684 | |||
a7fa6d00c1 | |||
49f377688a | |||
0b96962157 | |||
ebce62629e | |||
7aacad3270 | |||
035b882db1 | |||
0872e9c3ae | |||
1a573d17c0 | |||
4f20c370f9 | |||
e4bb248142 | |||
dff6268d66 | |||
8f9aa1a250 | |||
7d2e8875e0 | |||
3515e3ee28 | |||
cf82814606 | |||
fc29d82614 | |||
75ced3e945 | |||
685dc78739 | |||
9daa5f9177 | |||
69fbfb939f | |||
f0ecaabd7d | |||
c16f49cf19 | |||
9411458689 | |||
8771872d86 | |||
cda9ae1e42 | |||
81d68cd478 | |||
4c9078cccc | |||
f51828d049 | |||
d97562f6e8 | |||
234484b6f8 | |||
3bd45c005b | |||
05b7c1fffa | |||
a332712275 | |||
b2d8bd08f8 | |||
217be24963 | |||
bf457cd4fc | |||
88a8e986eb | |||
5f0567f8df | |||
a980b9d0a6 | |||
08504f6e06 | |||
da66484578 | |||
424efdaafe | |||
a65a7df209 | |||
c63bb81c3e | |||
a70e77ba48 | |||
c8b5909ee8 | |||
3b0ba923e4 | |||
1940b36e07 | |||
dfec687a46 | |||
bcd85b6f3e | |||
c4b919b24c | |||
c560bac13f | |||
88d27fd607 | |||
3d5f853b03 | |||
07a37f9b47 | |||
0172ad8461 | |||
e1f74a6d57 | |||
e17f6d654c | |||
817830940b | |||
dc9e8161d9 | |||
7f61cbbfd6 | |||
acca56f77c | |||
6bc695f251 | |||
91bb566ee6 | |||
5f04bbbb8b | |||
49fb5cb1a8 | |||
6e036ca09a | |||
8d1e36fa3c | |||
bccff3b237 | |||
a13a024ac8 | |||
5e7263cd1a | |||
0aafc29fb5 | |||
bd37473515 | |||
1c18e37a7c | |||
547c436281 | |||
e0c0d39ede | |||
4edce44689 | |||
186c08467f | |||
367fb9b504 | |||
ac75562296 | |||
7a9b14b49d | |||
32196cfe78 | |||
4d3283e235 | |||
dd3a3a2717 | |||
83d8e936ad | |||
58576630db | |||
7c84634e3f | |||
671640b0a9 | |||
5f7082f053 | |||
2a90cb7355 | |||
e63976df7e | |||
d8c2493658 | |||
4ed25b63a6 | |||
b318d588fe | |||
42d2adc3e0 | |||
5d1eb031eb | |||
1e7840c376 | |||
a6e3470c6f | |||
582b5f45e8 | |||
eb0b6c87d6 | |||
b6ce907928 | |||
9cffbdb42a | |||
d69e131450 | |||
6e84ba182e | |||
6773dfce8d | |||
13ce9e4f64 | |||
f63f8cb154 | |||
6e1118681d | |||
e5cec8f4eb | |||
6c36bd822c | |||
029c586717 | |||
ea6493c041 | |||
455d32d9e5 | |||
7bd801a167 | |||
b6e84879b6 | |||
f7832c0e82 | |||
8c1ab7e0a3 | |||
9d0f69ac50 | |||
215ca6c5ca | |||
a04c90e22d | |||
a84d410f11 | |||
636bae2466 | |||
739a7ea730 | |||
3893fbb0b1 | |||
948205c8e6 | |||
6278afde8d | |||
f0cb2dafbb | |||
a3c145432e | |||
e6f55da080 | |||
30f98f7e64 | |||
c9409a2edb | |||
b857064d65 | |||
a541382776 | |||
07ad24ab97 | |||
55db643048 | |||
8f9b198d48 | |||
6c7129cc0c | |||
919d55f3fc | |||
bdf63420d1 | |||
b7af715f6b | |||
b6eda33438 | |||
ab641d9f18 | |||
c7e128eed1 | |||
cc0259bbed | |||
23fba6d2ea | |||
3182adb6a0 | |||
d52ec65f18 | |||
b968376be9 | |||
90bd8c82b7 | |||
0955e8c5b6 | |||
ef55367224 | |||
a60f454154 | |||
7a7df3e635 | |||
62198a29c2 | |||
e87a35104a | |||
1e051e573d | |||
e172a621f3 | |||
9f09930834 | |||
20c2de9eed | |||
22ca5a6b8d | |||
8b19399b13 | |||
d289c773d0 | |||
a935e0720f | |||
1c3ff179bc | |||
ccab3d6b6e | |||
3e39fae6e1 | |||
d575fd1c3a | |||
0a2fb137af | |||
4907575d3d | |||
4200df21d3 | |||
e0bb5a2bd2 | |||
a6c2c685bc | |||
1e2fa68db0 | |||
599f16f15c | |||
91da168251 | |||
e104bccfb9 | |||
74bd0e32cc | |||
03015ed33f | |||
79ea70d4ec | |||
3ec76af96e | |||
b8efd2a347 | |||
9083157baa | |||
6cdc9e3b77 | |||
f8d4adfb7a | |||
719d9aa83c | |||
9ebaa737aa | |||
88b0982dac | |||
8c2e12ad79 | |||
2c31b3db07 | |||
eedf833b6f | |||
69d81cc065 | |||
af9c31152a | |||
abb6fca5e3 | |||
3ec1c40320 | |||
619211c1bf | |||
3a685049da | |||
ae54d05930 | |||
e7c4597ad0 | |||
09c9495015 | |||
e05f387632 | |||
9870c7c9a6 | |||
3f75b6b371 | |||
04fed82e5e | |||
f3a1dfef95 | |||
f738932bbd | |||
4968b6b9d0 | |||
ee97c00818 | |||
1dbd431117 | |||
09ab583f64 | |||
9ad6d13982 | |||
8d4426f2f8 | |||
8c8f795e9e | |||
7f2f67238f | |||
740fe942c1 | |||
7c5dcbb3fc | |||
7e055810b1 | |||
5758993e9f | |||
d7014e671d | |||
b0427ca9ff | |||
3af575cce7 | |||
f787d272e6 | |||
f061c9a30e | |||
8812072f06 |
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@ -162,3 +162,34 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "no changes in working directory";
|
echo "no changes in working directory";
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
build-wasm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain and cache
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||||
|
|
||||||
|
- name: Add wasm32-unknown-unknown target
|
||||||
|
run: rustup target add wasm32-unknown-unknown
|
||||||
|
|
||||||
|
- run: cargo build -p nu-cmd-base --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-cmd-extra --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-cmd-lang --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-color-config --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-command --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-derive-value --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-engine --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-glob --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-json --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-parser --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-path --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-pretty-hex --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-protocol --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-std --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-system --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-table --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-term-grid --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nu-utils --no-default-features --target wasm32-unknown-unknown
|
||||||
|
- run: cargo build -p nuon --no-default-features --target wasm32-unknown-unknown
|
||||||
|
18
.github/workflows/milestone.yml
vendored
18
.github/workflows/milestone.yml
vendored
@ -1,8 +1,11 @@
|
|||||||
# Description:
|
# Description:
|
||||||
# - Update milestone of a merged PR
|
# - Add milestone to a merged PR automatically
|
||||||
|
# - Add milestone to a closed issue that has a merged PR fix (if any)
|
||||||
|
|
||||||
name: Milestone Action
|
name: Milestone Action
|
||||||
on:
|
on:
|
||||||
|
issues:
|
||||||
|
types: [closed]
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
|
|
||||||
@ -10,9 +13,18 @@ jobs:
|
|||||||
update-milestone:
|
update-milestone:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Milestone Update
|
name: Milestone Update
|
||||||
if: ${{github.event.pull_request.merged == true}}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Milestone
|
- name: Set Milestone for PR
|
||||||
uses: hustcer/milestone-action@main
|
uses: hustcer/milestone-action@main
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Bind milestone to closed issue that has a merged PR fix
|
||||||
|
- name: Set Milestone for Issue
|
||||||
|
uses: hustcer/milestone-action@main
|
||||||
|
if: github.event.issue.state == 'closed'
|
||||||
|
with:
|
||||||
|
action: bind-issue
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
2
.github/workflows/nightly-build.yml
vendored
2
.github/workflows/nightly-build.yml
vendored
@ -170,7 +170,7 @@ jobs:
|
|||||||
# REF: https://github.com/marketplace/actions/gh-release
|
# REF: https://github.com/marketplace/actions/gh-release
|
||||||
# Create a release only in nushell/nightly repo
|
# Create a release only in nushell/nightly repo
|
||||||
- name: Publish Archive
|
- name: Publish Archive
|
||||||
uses: softprops/action-gh-release@v2.0.8
|
uses: softprops/action-gh-release@v2.0.9
|
||||||
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
|
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@ -98,9 +98,10 @@ jobs:
|
|||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
_EXTRA_: ${{ matrix.extra }}
|
_EXTRA_: ${{ matrix.extra }}
|
||||||
|
|
||||||
# REF: https://github.com/marketplace/actions/gh-release
|
# WARN: Don't upgrade this action due to the release per asset issue.
|
||||||
|
# See: https://github.com/softprops/action-gh-release/issues/445
|
||||||
- name: Publish Archive
|
- name: Publish Archive
|
||||||
uses: softprops/action-gh-release@v2.0.8
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
@ -124,7 +125,7 @@ jobs:
|
|||||||
- name: Create Checksums
|
- name: Create Checksums
|
||||||
run: cd release && shasum -a 256 * > ../SHA256SUMS
|
run: cd release && shasum -a 256 * > ../SHA256SUMS
|
||||||
- name: Publish Checksums
|
- name: Publish Checksums
|
||||||
uses: softprops/action-gh-release@v2.0.8
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
files: SHA256SUMS
|
files: SHA256SUMS
|
||||||
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@ -10,4 +10,4 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Check spelling
|
- name: Check spelling
|
||||||
uses: crate-ci/typos@v1.26.0
|
uses: crate-ci/typos@v1.28.4
|
||||||
|
2931
Cargo.lock
generated
2931
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
124
Cargo.toml
124
Cargo.toml
@ -10,8 +10,8 @@ homepage = "https://www.nushell.sh"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu"
|
name = "nu"
|
||||||
repository = "https://github.com/nushell/nushell"
|
repository = "https://github.com/nushell/nushell"
|
||||||
rust-version = "1.79.0"
|
rust-version = "1.81.0"
|
||||||
version = "0.99.1"
|
version = "0.101.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -66,16 +66,16 @@ alphanumeric-sort = "1.5"
|
|||||||
ansi-str = "0.8"
|
ansi-str = "0.8"
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bracoxide = "0.1.2"
|
bracoxide = "0.1.4"
|
||||||
brotli = "5.0"
|
brotli = "6.0"
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytesize = "1.3"
|
bytesize = "1.3"
|
||||||
calamine = "0.24.0"
|
calamine = "0.26.1"
|
||||||
chardetng = "0.1.17"
|
chardetng = "0.1.17"
|
||||||
chrono = { default-features = false, version = "0.4.34" }
|
chrono = { default-features = false, version = "0.4.34" }
|
||||||
chrono-humanize = "0.2.3"
|
chrono-humanize = "0.2.3"
|
||||||
chrono-tz = "0.8"
|
chrono-tz = "0.10"
|
||||||
crossbeam-channel = "0.5.8"
|
crossbeam-channel = "0.5.8"
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
csv = "1.3"
|
csv = "1.3"
|
||||||
@ -86,13 +86,13 @@ dirs = "5.0"
|
|||||||
dirs-sys = "0.4"
|
dirs-sys = "0.4"
|
||||||
dtparse = "2.0"
|
dtparse = "2.0"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
fancy-regex = "0.13"
|
fancy-regex = "0.14"
|
||||||
filesize = "0.2"
|
filesize = "0.2"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
fuzzy-matcher = "0.3"
|
fuzzy-matcher = "0.3"
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
human-date-parser = "0.2.0"
|
human-date-parser = "0.2.0"
|
||||||
indexmap = "2.6"
|
indexmap = "2.7"
|
||||||
indicatif = "0.17"
|
indicatif = "0.17"
|
||||||
interprocess = "2.2.0"
|
interprocess = "2.2.0"
|
||||||
is_executable = "1.0"
|
is_executable = "1.0"
|
||||||
@ -103,22 +103,22 @@ log = "0.4"
|
|||||||
lru = "0.12"
|
lru = "0.12"
|
||||||
lscolors = { version = "0.17", default-features = false }
|
lscolors = { version = "0.17", default-features = false }
|
||||||
lsp-server = "0.7.5"
|
lsp-server = "0.7.5"
|
||||||
lsp-types = "0.95.0"
|
lsp-types = { version = "0.95.0", features = ["proposed"] }
|
||||||
mach2 = "0.4"
|
mach2 = "0.4"
|
||||||
md5 = { version = "0.10", package = "md-5" }
|
md5 = { version = "0.10", package = "md-5" }
|
||||||
miette = "7.2"
|
miette = "7.3"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
mockito = { version = "1.5", default-features = false }
|
mockito = { version = "1.6", default-features = false }
|
||||||
multipart-rs = "0.1.11"
|
multipart-rs = "0.1.13"
|
||||||
native-tls = "0.2"
|
native-tls = "0.2"
|
||||||
nix = { version = "0.29", default-features = false }
|
nix = { version = "0.29", default-features = false }
|
||||||
notify-debouncer-full = { version = "0.3", default-features = false }
|
notify-debouncer-full = { version = "0.3", default-features = false }
|
||||||
nu-ansi-term = "0.50.1"
|
nu-ansi-term = "0.50.1"
|
||||||
num-format = "0.4"
|
num-format = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
|
oem_cp = "2.0.0"
|
||||||
omnipath = "0.1"
|
omnipath = "0.1"
|
||||||
once_cell = "1.20"
|
|
||||||
open = "5.3"
|
open = "5.3"
|
||||||
os_pipe = { version = "1.2", features = ["io_safety"] }
|
os_pipe = { version = "1.2", features = ["io_safety"] }
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
@ -127,25 +127,27 @@ pretty_assertions = "1.4"
|
|||||||
print-positions = "0.6"
|
print-positions = "0.6"
|
||||||
proc-macro-error = { version = "1.0", default-features = false }
|
proc-macro-error = { version = "1.0", default-features = false }
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
procfs = "0.16.0"
|
procfs = "0.17.0"
|
||||||
pwd = "1.3"
|
pwd = "1.3"
|
||||||
quick-xml = "0.32.0"
|
quick-xml = "0.37.0"
|
||||||
quickcheck = "1.0"
|
quickcheck = "1.0"
|
||||||
quickcheck_macros = "1.0"
|
quickcheck_macros = "1.0"
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
getrandom = "0.2" # pick same version that rand requires
|
||||||
rand_chacha = "0.3.1"
|
rand_chacha = "0.3.1"
|
||||||
ratatui = "0.26"
|
ratatui = "0.26"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
reedline = "0.36.0"
|
reedline = "0.38.0"
|
||||||
regex = "1.9.5"
|
regex = "1.9.5"
|
||||||
rmp = "0.8"
|
rmp = "0.8"
|
||||||
rmp-serde = "1.3"
|
rmp-serde = "1.3"
|
||||||
ropey = "1.6.1"
|
ropey = "1.6.1"
|
||||||
roxmltree = "0.19"
|
roxmltree = "0.20"
|
||||||
rstest = { version = "0.18", default-features = false }
|
rstest = { version = "0.23", default-features = false }
|
||||||
rusqlite = "0.31"
|
rusqlite = "0.31"
|
||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
|
scopeguard = { version = "1.2.0" }
|
||||||
serde = { version = "1.0" }
|
serde = { version = "1.0" }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
@ -153,30 +155,31 @@ serde_yaml = "0.9"
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
strip-ansi-escapes = "0.2.0"
|
strip-ansi-escapes = "0.2.0"
|
||||||
syn = "2.0"
|
syn = "2.0"
|
||||||
sysinfo = "0.30"
|
sysinfo = "0.32"
|
||||||
tabled = { version = "0.16.0", default-features = false }
|
tabled = { version = "0.16.0", default-features = false }
|
||||||
tempfile = "3.13"
|
tempfile = "3.14"
|
||||||
terminal_size = "0.3"
|
terminal_size = "0.4"
|
||||||
titlecase = "2.0"
|
titlecase = "3.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
trash = "3.3"
|
trash = "5.2"
|
||||||
umask = "2.1"
|
umask = "2.1"
|
||||||
unicode-segmentation = "1.12"
|
unicode-segmentation = "1.12"
|
||||||
unicode-width = "0.1"
|
unicode-width = "0.2"
|
||||||
ureq = { version = "2.10", default-features = false }
|
ureq = { version = "2.12", default-features = false }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
uu_cp = "0.0.27"
|
uu_cp = "0.0.28"
|
||||||
uu_mkdir = "0.0.27"
|
uu_mkdir = "0.0.28"
|
||||||
uu_mktemp = "0.0.27"
|
uu_mktemp = "0.0.28"
|
||||||
uu_mv = "0.0.27"
|
uu_mv = "0.0.28"
|
||||||
uu_whoami = "0.0.27"
|
uu_touch = "0.0.28"
|
||||||
uu_uname = "0.0.27"
|
uu_whoami = "0.0.28"
|
||||||
uucore = "0.0.27"
|
uu_uname = "0.0.28"
|
||||||
uuid = "1.10.0"
|
uucore = "0.0.28"
|
||||||
|
uuid = "1.11.0"
|
||||||
v_htmlescape = "0.15.0"
|
v_htmlescape = "0.15.0"
|
||||||
wax = "0.6"
|
wax = "0.6"
|
||||||
which = "6.0.0"
|
which = "7.0.0"
|
||||||
windows = "0.54"
|
windows = "0.56"
|
||||||
windows-sys = "0.48"
|
windows-sys = "0.48"
|
||||||
winreg = "0.52"
|
winreg = "0.52"
|
||||||
|
|
||||||
@ -189,22 +192,22 @@ unchecked_duration_subtraction = "warn"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cli = { path = "./crates/nu-cli", version = "0.99.1" }
|
nu-cli = { path = "./crates/nu-cli", version = "0.101.0" }
|
||||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.99.1" }
|
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.101.0" }
|
||||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.99.1" }
|
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.101.0" }
|
||||||
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.99.1", optional = true }
|
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.101.0", optional = true }
|
||||||
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.99.1" }
|
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.101.0" }
|
||||||
nu-command = { path = "./crates/nu-command", version = "0.99.1" }
|
nu-command = { path = "./crates/nu-command", version = "0.101.0" }
|
||||||
nu-engine = { path = "./crates/nu-engine", version = "0.99.1" }
|
nu-engine = { path = "./crates/nu-engine", version = "0.101.0" }
|
||||||
nu-explore = { path = "./crates/nu-explore", version = "0.99.1" }
|
nu-explore = { path = "./crates/nu-explore", version = "0.101.0" }
|
||||||
nu-lsp = { path = "./crates/nu-lsp/", version = "0.99.1" }
|
nu-lsp = { path = "./crates/nu-lsp/", version = "0.101.0" }
|
||||||
nu-parser = { path = "./crates/nu-parser", version = "0.99.1" }
|
nu-parser = { path = "./crates/nu-parser", version = "0.101.0" }
|
||||||
nu-path = { path = "./crates/nu-path", version = "0.99.1" }
|
nu-path = { path = "./crates/nu-path", version = "0.101.0" }
|
||||||
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.99.1" }
|
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.101.0" }
|
||||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.99.1" }
|
nu-protocol = { path = "./crates/nu-protocol", version = "0.101.0" }
|
||||||
nu-std = { path = "./crates/nu-std", version = "0.99.1" }
|
nu-std = { path = "./crates/nu-std", version = "0.101.0" }
|
||||||
nu-system = { path = "./crates/nu-system", version = "0.99.1" }
|
nu-system = { path = "./crates/nu-system", version = "0.101.0" }
|
||||||
nu-utils = { path = "./crates/nu-utils", version = "0.99.1" }
|
nu-utils = { path = "./crates/nu-utils", version = "0.101.0" }
|
||||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||||
|
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
@ -234,27 +237,32 @@ nix = { workspace = true, default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-test-support = { path = "./crates/nu-test-support", version = "0.99.1" }
|
nu-test-support = { path = "./crates/nu-test-support", version = "0.101.0" }
|
||||||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.99.1" }
|
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.101.0" }
|
||||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.99.1" }
|
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.101.0" }
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
tango-bench = "0.6"
|
tango-bench = "0.6"
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rstest = { workspace = true, default-features = false }
|
rstest = { workspace = true, default-features = false }
|
||||||
serial_test = "3.1"
|
serial_test = "3.2"
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
plugin = [
|
plugin = [
|
||||||
"nu-plugin-engine",
|
# crates
|
||||||
"nu-cmd-plugin",
|
"nu-cmd-plugin",
|
||||||
|
"nu-plugin-engine",
|
||||||
|
|
||||||
|
# features
|
||||||
"nu-cli/plugin",
|
"nu-cli/plugin",
|
||||||
"nu-parser/plugin",
|
"nu-cmd-lang/plugin",
|
||||||
"nu-command/plugin",
|
"nu-command/plugin",
|
||||||
"nu-protocol/plugin",
|
|
||||||
"nu-engine/plugin",
|
"nu-engine/plugin",
|
||||||
|
"nu-engine/plugin",
|
||||||
|
"nu-parser/plugin",
|
||||||
|
"nu-protocol/plugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
default = [
|
default = [
|
||||||
|
@ -58,7 +58,7 @@ For details about which platforms the Nushell team actively supports, see [our p
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The default configurations can be found at [sample_config](crates/nu-utils/src/sample_config)
|
The default configurations can be found at [sample_config](crates/nu-utils/src/default_files)
|
||||||
which are the configuration files one gets when they startup Nushell for the first time.
|
which are the configuration files one gets when they startup Nushell for the first time.
|
||||||
|
|
||||||
It sets all of the default configuration to run Nushell. From here one can
|
It sets all of the default configuration to run Nushell. From here one can
|
||||||
@ -229,7 +229,7 @@ Please submit an issue or PR to be added to this list.
|
|||||||
See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed!
|
See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed!
|
||||||
|
|
||||||
<a href="https://github.com/nushell/nushell/graphs/contributors">
|
<a href="https://github.com/nushell/nushell/graphs/contributors">
|
||||||
<img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=750" />
|
<img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=750&columns=20" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -46,9 +46,6 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
|
|||||||
|
|
||||||
let mut stack = Stack::new();
|
let mut stack = Stack::new();
|
||||||
|
|
||||||
// Support running benchmarks without IR mode
|
|
||||||
stack.use_ir = std::env::var_os("NU_DISABLE_IR").is_none();
|
|
||||||
|
|
||||||
evaluate_commands(
|
evaluate_commands(
|
||||||
&commands,
|
&commands,
|
||||||
&mut engine,
|
&mut engine,
|
||||||
|
@ -5,27 +5,27 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cli"
|
name = "nu-cli"
|
||||||
version = "0.99.1"
|
version = "0.101.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.99.1" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.101.0" }
|
||||||
nu-command = { path = "../nu-command", version = "0.99.1" }
|
nu-command = { path = "../nu-command", version = "0.101.0" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.99.1" }
|
nu-test-support = { path = "../nu-test-support", version = "0.101.0" }
|
||||||
rstest = { workspace = true, default-features = false }
|
rstest = { workspace = true, default-features = false }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.99.1" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.101.0" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
nu-engine = { path = "../nu-engine", version = "0.101.0", features = ["os"] }
|
||||||
nu-path = { path = "../nu-path", version = "0.99.1" }
|
nu-path = { path = "../nu-path", version = "0.101.0" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
nu-parser = { path = "../nu-parser", version = "0.101.0" }
|
||||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.99.1", optional = true }
|
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.101.0", optional = true }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
nu-protocol = { path = "../nu-protocol", version = "0.101.0", features = ["os"] }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.99.1" }
|
nu-utils = { path = "../nu-utils", version = "0.101.0" }
|
||||||
nu-color-config = { path = "../nu-color-config", version = "0.99.1" }
|
nu-color-config = { path = "../nu-color-config", version = "0.101.0" }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||||
|
|
||||||
@ -37,7 +37,6 @@ is_executable = { workspace = true }
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||||
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
|
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
|
||||||
once_cell = { workspace = true }
|
|
||||||
percent-encoding = { workspace = true }
|
percent-encoding = { workspace = true }
|
||||||
sysinfo = { workspace = true }
|
sysinfo = { workspace = true }
|
||||||
unicode-segmentation = { workspace = true }
|
unicode-segmentation = { workspace = true }
|
||||||
|
@ -17,6 +17,7 @@ pub fn add_cli_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
CommandlineGetCursor,
|
CommandlineGetCursor,
|
||||||
CommandlineSetCursor,
|
CommandlineSetCursor,
|
||||||
History,
|
History,
|
||||||
|
HistoryImport,
|
||||||
HistorySession,
|
HistorySession,
|
||||||
Keybindings,
|
Keybindings,
|
||||||
KeybindingsDefault,
|
KeybindingsDefault,
|
||||||
|
9
crates/nu-cli/src/commands/history/fields.rs
Normal file
9
crates/nu-cli/src/commands/history/fields.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Each const is named after a HistoryItem field, and the value is the field name to be displayed to
|
||||||
|
// the user (or accept during import).
|
||||||
|
pub const COMMAND_LINE: &str = "command";
|
||||||
|
pub const START_TIMESTAMP: &str = "start_timestamp";
|
||||||
|
pub const HOSTNAME: &str = "hostname";
|
||||||
|
pub const CWD: &str = "cwd";
|
||||||
|
pub const EXIT_STATUS: &str = "exit_status";
|
||||||
|
pub const DURATION: &str = "duration";
|
||||||
|
pub const SESSION_ID: &str = "session_id";
|
@ -5,6 +5,8 @@ use reedline::{
|
|||||||
SqliteBackedHistory,
|
SqliteBackedHistory,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::fields;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct History;
|
pub struct History;
|
||||||
|
|
||||||
@ -83,7 +85,8 @@ impl Command for History {
|
|||||||
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
||||||
Value::record(
|
Value::record(
|
||||||
record! {
|
record! {
|
||||||
"command" => Value::string(entry.command_line, head),
|
fields::COMMAND_LINE => Value::string(entry.command_line, head),
|
||||||
|
// TODO: This name is inconsistent with create_history_record.
|
||||||
"index" => Value::int(idx as i64, head),
|
"index" => Value::int(idx as i64, head),
|
||||||
},
|
},
|
||||||
head,
|
head,
|
||||||
@ -176,13 +179,13 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span)
|
|||||||
Value::record(
|
Value::record(
|
||||||
record! {
|
record! {
|
||||||
"item_id" => item_id_value,
|
"item_id" => item_id_value,
|
||||||
"start_timestamp" => start_timestamp_value,
|
fields::START_TIMESTAMP => start_timestamp_value,
|
||||||
"command" => command_value,
|
fields::COMMAND_LINE => command_value,
|
||||||
"session_id" => session_id_value,
|
fields::SESSION_ID => session_id_value,
|
||||||
"hostname" => hostname_value,
|
fields::HOSTNAME => hostname_value,
|
||||||
"cwd" => cwd_value,
|
fields::CWD => cwd_value,
|
||||||
"duration" => duration_value,
|
fields::DURATION => duration_value,
|
||||||
"exit_status" => exit_status_value,
|
fields::EXIT_STATUS => exit_status_value,
|
||||||
"idx" => index_value,
|
"idx" => index_value,
|
||||||
},
|
},
|
||||||
head,
|
head,
|
||||||
@ -190,11 +193,11 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span)
|
|||||||
} else {
|
} else {
|
||||||
Value::record(
|
Value::record(
|
||||||
record! {
|
record! {
|
||||||
"start_timestamp" => start_timestamp_value,
|
fields::START_TIMESTAMP => start_timestamp_value,
|
||||||
"command" => command_value,
|
fields::COMMAND_LINE => command_value,
|
||||||
"cwd" => cwd_value,
|
fields::CWD => cwd_value,
|
||||||
"duration" => duration_value,
|
fields::DURATION => duration_value,
|
||||||
"exit_status" => exit_status_value,
|
fields::EXIT_STATUS => exit_status_value,
|
||||||
},
|
},
|
||||||
head,
|
head,
|
||||||
)
|
)
|
||||||
|
415
crates/nu-cli/src/commands/history/history_import.rs
Normal file
415
crates/nu-cli/src/commands/history/history_import.rs
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::HistoryFileFormat;
|
||||||
|
|
||||||
|
use reedline::{
|
||||||
|
FileBackedHistory, History, HistoryItem, ReedlineError, SearchQuery, SqliteBackedHistory,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::fields;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HistoryImport;
|
||||||
|
|
||||||
|
impl Command for HistoryImport {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"history import"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Import command line history"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"Can import history from input, either successive command lines or more detailed records. If providing records, available fields are:
|
||||||
|
command_line, id, start_timestamp, hostname, cwd, duration, exit_status.
|
||||||
|
|
||||||
|
If no input is provided, will import all history items from existing history in the other format: if current history is stored in sqlite, it will store it in plain text and vice versa.
|
||||||
|
|
||||||
|
Note that history item IDs are ignored when importing from file."#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("history import")
|
||||||
|
.category(Category::History)
|
||||||
|
.input_output_types(vec![
|
||||||
|
(Type::Nothing, Type::Nothing),
|
||||||
|
(Type::List(Box::new(Type::String)), Type::Nothing),
|
||||||
|
(Type::table(), Type::Nothing),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
example: "history import",
|
||||||
|
description:
|
||||||
|
"Append all items from history in the other format to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "echo foo | history import",
|
||||||
|
description: "Append `foo` to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "[[ command_line cwd ]; [ foo /home ]] | history import",
|
||||||
|
description: "Append `foo` ran from `/home` to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let ok = Ok(Value::nothing(call.head).into_pipeline_data());
|
||||||
|
|
||||||
|
let Some(history) = engine_state.history_config() else {
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
let Some(current_history_path) = history.file_path() else {
|
||||||
|
return Err(ShellError::ConfigDirNotFound {
|
||||||
|
span: Some(call.head),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if let Some(bak_path) = backup(¤t_history_path)? {
|
||||||
|
println!("Backed history to {}", bak_path.display());
|
||||||
|
}
|
||||||
|
match input {
|
||||||
|
PipelineData::Empty => {
|
||||||
|
let other_format = match history.file_format {
|
||||||
|
HistoryFileFormat::Sqlite => HistoryFileFormat::Plaintext,
|
||||||
|
HistoryFileFormat::Plaintext => HistoryFileFormat::Sqlite,
|
||||||
|
};
|
||||||
|
let src = new_backend(other_format, None)?;
|
||||||
|
let mut dst = new_backend(history.file_format, Some(current_history_path))?;
|
||||||
|
let items = src
|
||||||
|
.search(SearchQuery::everything(
|
||||||
|
reedline::SearchDirection::Forward,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.map_err(error_from_reedline)?
|
||||||
|
.into_iter()
|
||||||
|
.map(Ok);
|
||||||
|
import(dst.as_mut(), items)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let input = input.into_iter().map(item_from_value);
|
||||||
|
import(
|
||||||
|
new_backend(history.file_format, Some(current_history_path))?.as_mut(),
|
||||||
|
input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_backend(
|
||||||
|
format: HistoryFileFormat,
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
) -> Result<Box<dyn History>, ShellError> {
|
||||||
|
let path = match path {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
let Some(mut path) = nu_path::nu_config_dir() else {
|
||||||
|
return Err(ShellError::ConfigDirNotFound { span: None });
|
||||||
|
};
|
||||||
|
path.push(format.default_file_name());
|
||||||
|
path.into_std_path_buf()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn map(
|
||||||
|
result: Result<impl History + 'static, ReedlineError>,
|
||||||
|
) -> Result<Box<dyn History>, ShellError> {
|
||||||
|
result
|
||||||
|
.map(|x| Box::new(x) as Box<dyn History>)
|
||||||
|
.map_err(error_from_reedline)
|
||||||
|
}
|
||||||
|
match format {
|
||||||
|
// Use a reasonably large value for maximum capacity.
|
||||||
|
HistoryFileFormat::Plaintext => map(FileBackedHistory::with_file(0xfffffff, path)),
|
||||||
|
HistoryFileFormat::Sqlite => map(SqliteBackedHistory::with_file(path, None, None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import(
|
||||||
|
dst: &mut dyn History,
|
||||||
|
src: impl Iterator<Item = Result<HistoryItem, ShellError>>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
for item in src {
|
||||||
|
let mut item = item?;
|
||||||
|
item.id = None;
|
||||||
|
dst.save(item).map_err(error_from_reedline)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_from_reedline(e: ReedlineError) -> ShellError {
|
||||||
|
// TODO: Should we add a new ShellError variant?
|
||||||
|
ShellError::GenericError {
|
||||||
|
error: "Reedline error".to_owned(),
|
||||||
|
msg: format!("{e}"),
|
||||||
|
span: None,
|
||||||
|
help: None,
|
||||||
|
inner: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_from_value(v: Value) -> Result<HistoryItem, ShellError> {
|
||||||
|
let span = v.span();
|
||||||
|
match v {
|
||||||
|
Value::Record { val, .. } => item_from_record(val.into_owned(), span),
|
||||||
|
Value::String { val, .. } => Ok(HistoryItem {
|
||||||
|
command_line: val,
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None,
|
||||||
|
}),
|
||||||
|
_ => Err(ShellError::UnsupportedInput {
|
||||||
|
msg: "Only list and record inputs are supported".to_owned(),
|
||||||
|
input: v.get_type().to_string(),
|
||||||
|
msg_span: span,
|
||||||
|
input_span: span,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_from_record(mut rec: Record, span: Span) -> Result<HistoryItem, ShellError> {
|
||||||
|
let cmd = match rec.remove(fields::COMMAND_LINE) {
|
||||||
|
Some(v) => v.as_str()?.to_owned(),
|
||||||
|
None => {
|
||||||
|
return Err(ShellError::TypeMismatch {
|
||||||
|
err_message: format!("missing column: {}", fields::COMMAND_LINE),
|
||||||
|
span,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get<T>(
|
||||||
|
rec: &mut Record,
|
||||||
|
field: &'static str,
|
||||||
|
f: impl FnOnce(Value) -> Result<T, ShellError>,
|
||||||
|
) -> Result<Option<T>, ShellError> {
|
||||||
|
rec.remove(field).map(f).transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
let rec = &mut rec;
|
||||||
|
let item = HistoryItem {
|
||||||
|
command_line: cmd,
|
||||||
|
id: None,
|
||||||
|
start_timestamp: get(rec, fields::START_TIMESTAMP, |v| Ok(v.as_date()?.to_utc()))?,
|
||||||
|
hostname: get(rec, fields::HOSTNAME, |v| Ok(v.as_str()?.to_owned()))?,
|
||||||
|
cwd: get(rec, fields::CWD, |v| Ok(v.as_str()?.to_owned()))?,
|
||||||
|
exit_status: get(rec, fields::EXIT_STATUS, |v| v.as_int())?,
|
||||||
|
duration: get(rec, fields::DURATION, duration_from_value)?,
|
||||||
|
more_info: None,
|
||||||
|
// TODO: Currently reedline doesn't let you create session IDs.
|
||||||
|
session_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !rec.is_empty() {
|
||||||
|
let cols = rec.columns().map(|s| s.as_str()).collect::<Vec<_>>();
|
||||||
|
return Err(ShellError::TypeMismatch {
|
||||||
|
err_message: format!("unsupported column names: {}", cols.join(", ")),
|
||||||
|
span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duration_from_value(v: Value) -> Result<std::time::Duration, ShellError> {
|
||||||
|
chrono::Duration::nanoseconds(v.as_duration()?)
|
||||||
|
.to_std()
|
||||||
|
.map_err(|_| ShellError::IOError {
|
||||||
|
msg: "negative duration not supported".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_backup_path(path: &Path) -> Result<PathBuf, ShellError> {
|
||||||
|
let Ok(mut bak_path) = path.to_path_buf().into_os_string().into_string() else {
|
||||||
|
// This isn't fundamentally problem, but trying to work with OsString is a nightmare.
|
||||||
|
return Err(ShellError::IOError {
|
||||||
|
msg: "History path mush be representable as UTF-8".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
bak_path.push_str(".bak");
|
||||||
|
if !Path::new(&bak_path).exists() {
|
||||||
|
return Ok(bak_path.into());
|
||||||
|
}
|
||||||
|
let base_len = bak_path.len();
|
||||||
|
for i in 1..100 {
|
||||||
|
use std::fmt::Write;
|
||||||
|
bak_path.truncate(base_len);
|
||||||
|
write!(&mut bak_path, ".{i}").unwrap();
|
||||||
|
if !Path::new(&bak_path).exists() {
|
||||||
|
return Ok(PathBuf::from(bak_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ShellError::IOError {
|
||||||
|
msg: "Too many existing backup files".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backup(path: &Path) -> Result<Option<PathBuf>, ShellError> {
|
||||||
|
match path.metadata() {
|
||||||
|
Ok(md) if md.is_file() => (),
|
||||||
|
Ok(_) => {
|
||||||
|
return Err(ShellError::IOError {
|
||||||
|
msg: "history path exists but is not a file".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
}
|
||||||
|
let bak_path = find_backup_path(path)?;
|
||||||
|
std::fs::copy(path, &bak_path)?;
|
||||||
|
Ok(Some(bak_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::DateTime;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_string() -> Result<(), ShellError> {
|
||||||
|
let item = item_from_value(Value::string("foo", Span::unknown()))?;
|
||||||
|
assert_eq!(
|
||||||
|
item,
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command", Value::string("foo", span)),
|
||||||
|
(
|
||||||
|
"start_timestamp",
|
||||||
|
Value::date(
|
||||||
|
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap(),
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("hostname", Value::string("localhost", span)),
|
||||||
|
("cwd", Value::string("/home/test", span)),
|
||||||
|
("duration", Value::duration(100_000_000, span)),
|
||||||
|
("exit_status", Value::int(42, span)),
|
||||||
|
]);
|
||||||
|
let item = item_from_value(rec).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
item,
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: Some(
|
||||||
|
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc()
|
||||||
|
),
|
||||||
|
hostname: Some("localhost".to_string()),
|
||||||
|
cwd: Some("/home/test".to_string()),
|
||||||
|
duration: Some(std::time::Duration::from_nanos(100_000_000)),
|
||||||
|
exit_status: Some(42),
|
||||||
|
|
||||||
|
session_id: None,
|
||||||
|
more_info: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record_extra_field() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command_line", Value::string("foo", span)),
|
||||||
|
("id_nonexistent", Value::int(1, span)),
|
||||||
|
]);
|
||||||
|
assert!(item_from_value(rec).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record_bad_type() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command_line", Value::string("foo", span)),
|
||||||
|
("id", Value::string("one".to_string(), span)),
|
||||||
|
]);
|
||||||
|
assert!(item_from_value(rec).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_record(rec: &[(&'static str, Value)]) -> Value {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = Record::from_raw_cols_vals(
|
||||||
|
rec.iter().map(|(col, _)| col.to_string()).collect(),
|
||||||
|
rec.iter().map(|(_, val)| val.clone()).collect(),
|
||||||
|
span,
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
Value::record(rec, span)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::no_backup(&["history.dat"], "history.dat.bak")]
|
||||||
|
#[case::backup_exists(&["history.dat", "history.dat.bak"], "history.dat.bak.1")]
|
||||||
|
#[case::multiple_backups_exists( &["history.dat", "history.dat.bak", "history.dat.bak.1"], "history.dat.bak.2")]
|
||||||
|
fn test_find_backup_path(#[case] existing: &[&str], #[case] want: &str) {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
for name in existing {
|
||||||
|
std::fs::File::create_new(dir.path().join(name)).unwrap();
|
||||||
|
}
|
||||||
|
let got = find_backup_path(&dir.path().join("history.dat")).unwrap();
|
||||||
|
assert_eq!(got, dir.path().join(want))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut history = std::fs::File::create_new(dir.path().join("history.dat")).unwrap();
|
||||||
|
use std::io::Write;
|
||||||
|
write!(&mut history, "123").unwrap();
|
||||||
|
let want_bak_path = dir.path().join("history.dat.bak");
|
||||||
|
assert_eq!(
|
||||||
|
backup(&dir.path().join("history.dat")),
|
||||||
|
Ok(Some(want_bak_path.clone()))
|
||||||
|
);
|
||||||
|
let got_data = String::from_utf8(std::fs::read(want_bak_path).unwrap()).unwrap();
|
||||||
|
assert_eq!(got_data, "123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup_no_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let bak_path = backup(&dir.path().join("history.dat")).unwrap();
|
||||||
|
assert!(bak_path.is_none());
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
|
mod fields;
|
||||||
mod history_;
|
mod history_;
|
||||||
|
mod history_import;
|
||||||
mod history_session;
|
mod history_session;
|
||||||
|
|
||||||
pub use history_::History;
|
pub use history_::History;
|
||||||
|
pub use history_import::HistoryImport;
|
||||||
pub use history_session::HistorySession;
|
pub use history_session::HistorySession;
|
||||||
|
@ -7,7 +7,7 @@ mod keybindings_list;
|
|||||||
mod keybindings_listen;
|
mod keybindings_listen;
|
||||||
|
|
||||||
pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor};
|
pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor};
|
||||||
pub use history::{History, HistorySession};
|
pub use history::{History, HistoryImport, HistorySession};
|
||||||
pub use keybindings::Keybindings;
|
pub use keybindings::Keybindings;
|
||||||
pub use keybindings_default::KeybindingsDefault;
|
pub use keybindings_default::KeybindingsDefault;
|
||||||
pub use keybindings_list::KeybindingsList;
|
pub use keybindings_list::KeybindingsList;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
completions::{Completer, CompletionOptions, MatchAlgorithm},
|
completions::{Completer, CompletionOptions},
|
||||||
SuggestionKind,
|
SuggestionKind,
|
||||||
};
|
};
|
||||||
use nu_parser::FlatShape;
|
use nu_parser::FlatShape;
|
||||||
@ -9,7 +11,7 @@ use nu_protocol::{
|
|||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
|
|
||||||
use super::{completion_common::sort_suggestions, SemanticSuggestion};
|
use super::{completion_options::NuMatcher, SemanticSuggestion};
|
||||||
|
|
||||||
pub struct CommandCompletion {
|
pub struct CommandCompletion {
|
||||||
flattened: Vec<(Span, FlatShape)>,
|
flattened: Vec<(Span, FlatShape)>,
|
||||||
@ -33,13 +35,13 @@ impl CommandCompletion {
|
|||||||
fn external_command_completion(
|
fn external_command_completion(
|
||||||
&self,
|
&self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
prefix: &str,
|
sugg_span: reedline::Span,
|
||||||
match_algorithm: MatchAlgorithm,
|
matched_internal: impl Fn(&str) -> bool,
|
||||||
) -> Vec<String> {
|
matcher: &mut NuMatcher<String>,
|
||||||
let mut executables = vec![];
|
) -> HashMap<String, SemanticSuggestion> {
|
||||||
|
let mut suggs = HashMap::new();
|
||||||
|
|
||||||
// os agnostic way to get the PATH env var
|
let paths = working_set.permanent_state.get_env_var_insensitive("path");
|
||||||
let paths = working_set.permanent_state.get_path_env_var();
|
|
||||||
|
|
||||||
if let Some(paths) = paths {
|
if let Some(paths) = paths {
|
||||||
if let Ok(paths) = paths.as_list() {
|
if let Ok(paths) = paths.as_list() {
|
||||||
@ -54,24 +56,38 @@ impl CommandCompletion {
|
|||||||
.completions
|
.completions
|
||||||
.external
|
.external
|
||||||
.max_results
|
.max_results
|
||||||
> executables.len() as i64
|
<= suggs.len() as i64
|
||||||
&& !executables.contains(
|
|
||||||
&item
|
|
||||||
.path()
|
|
||||||
.file_name()
|
|
||||||
.map(|x| x.to_string_lossy().to_string())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
&& matches!(
|
|
||||||
item.path().file_name().map(|x| match_algorithm
|
|
||||||
.matches_str(&x.to_string_lossy(), prefix)),
|
|
||||||
Some(true)
|
|
||||||
)
|
|
||||||
&& is_executable::is_executable(item.path())
|
|
||||||
{
|
{
|
||||||
if let Ok(name) = item.file_name().into_string() {
|
break;
|
||||||
executables.push(name);
|
|
||||||
}
|
}
|
||||||
|
let Ok(name) = item.file_name().into_string() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value = if matched_internal(&name) {
|
||||||
|
format!("^{}", name)
|
||||||
|
} else {
|
||||||
|
name.clone()
|
||||||
|
};
|
||||||
|
if suggs.contains_key(&value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if matcher.matches(&name) && is_executable::is_executable(item.path()) {
|
||||||
|
// If there's an internal command with the same name, adds ^cmd to the
|
||||||
|
// matcher so that both the internal and external command are included
|
||||||
|
matcher.add(&name, value.clone());
|
||||||
|
suggs.insert(
|
||||||
|
value.clone(),
|
||||||
|
SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
span: sugg_span,
|
||||||
|
append_whitespace: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
// TODO: is there a way to create a test?
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,7 +95,7 @@ impl CommandCompletion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
executables
|
suggs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_commands(
|
fn complete_commands(
|
||||||
@ -88,69 +104,60 @@ impl CommandCompletion {
|
|||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
find_externals: bool,
|
find_externals: bool,
|
||||||
match_algorithm: MatchAlgorithm,
|
options: &CompletionOptions,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
let partial = working_set.get_span_contents(span);
|
let partial = working_set.get_span_contents(span);
|
||||||
|
let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone());
|
||||||
|
|
||||||
let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial);
|
let sugg_span = reedline::Span::new(span.start - offset, span.end - offset);
|
||||||
|
|
||||||
let mut results = working_set
|
let mut internal_suggs = HashMap::new();
|
||||||
.find_commands_by_predicate(filter_predicate, true)
|
let filtered_commands = working_set.find_commands_by_predicate(
|
||||||
.into_iter()
|
|name| {
|
||||||
.map(move |x| SemanticSuggestion {
|
let name = String::from_utf8_lossy(name);
|
||||||
|
matcher.add(&name, name.to_string())
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
for (name, description, typ) in filtered_commands {
|
||||||
|
let name = String::from_utf8_lossy(&name);
|
||||||
|
internal_suggs.insert(
|
||||||
|
name.to_string(),
|
||||||
|
SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(&x.0).to_string(),
|
value: name.to_string(),
|
||||||
description: x.1,
|
description,
|
||||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
span: sugg_span,
|
||||||
append_whitespace: true,
|
append_whitespace: true,
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
},
|
},
|
||||||
kind: Some(SuggestionKind::Command(x.2)),
|
kind: Some(SuggestionKind::Command(typ)),
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let partial = working_set.get_span_contents(span);
|
|
||||||
let partial = String::from_utf8_lossy(partial).to_string();
|
|
||||||
|
|
||||||
if find_externals {
|
|
||||||
let results_external = self
|
|
||||||
.external_command_completion(working_set, &partial, match_algorithm)
|
|
||||||
.into_iter()
|
|
||||||
.map(move |x| SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
|
||||||
value: x,
|
|
||||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
|
||||||
append_whitespace: true,
|
|
||||||
..Suggestion::default()
|
|
||||||
},
|
},
|
||||||
// TODO: is there a way to create a test?
|
);
|
||||||
kind: None,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
let results_strings: Vec<String> =
|
let mut external_suggs = if find_externals {
|
||||||
results.iter().map(|x| x.suggestion.value.clone()).collect();
|
self.external_command_completion(
|
||||||
|
working_set,
|
||||||
for external in results_external {
|
sugg_span,
|
||||||
if results_strings.contains(&external.suggestion.value) {
|
|name| internal_suggs.contains_key(name),
|
||||||
results.push(SemanticSuggestion {
|
&mut matcher,
|
||||||
suggestion: Suggestion {
|
)
|
||||||
value: format!("^{}", external.suggestion.value),
|
|
||||||
span: external.suggestion.span,
|
|
||||||
append_whitespace: true,
|
|
||||||
..Suggestion::default()
|
|
||||||
},
|
|
||||||
kind: external.kind,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
results.push(external)
|
HashMap::new()
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
results
|
let mut res = Vec::new();
|
||||||
} else {
|
for cmd_name in matcher.results() {
|
||||||
results
|
if let Some(sugg) = internal_suggs
|
||||||
|
.remove(&cmd_name)
|
||||||
|
.or_else(|| external_suggs.remove(&cmd_name))
|
||||||
|
{
|
||||||
|
res.push(sugg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for CommandCompletion {
|
impl Completer for CommandCompletion {
|
||||||
@ -158,7 +165,7 @@ impl Completer for CommandCompletion {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
_stack: &Stack,
|
_stack: &Stack,
|
||||||
prefix: &[u8],
|
_prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
@ -188,18 +195,18 @@ impl Completer for CommandCompletion {
|
|||||||
Span::new(last.0.start, pos),
|
Span::new(last.0.start, pos),
|
||||||
offset,
|
offset,
|
||||||
false,
|
false,
|
||||||
options.match_algorithm,
|
options,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
if !subcommands.is_empty() {
|
if !subcommands.is_empty() {
|
||||||
return sort_suggestions(&String::from_utf8_lossy(prefix), subcommands, options);
|
return subcommands;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = working_set.get_config();
|
let config = working_set.get_config();
|
||||||
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External)
|
if matches!(self.flat_shape, nu_parser::FlatShape::External)
|
||||||
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_))
|
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_))
|
||||||
|| ((span.end - span.start) == 0)
|
|| ((span.end - span.start) == 0)
|
||||||
|| is_passthrough_command(working_set.delta.get_file_contents())
|
|| is_passthrough_command(working_set.delta.get_file_contents())
|
||||||
@ -214,13 +221,11 @@ impl Completer for CommandCompletion {
|
|||||||
span,
|
span,
|
||||||
offset,
|
offset,
|
||||||
config.completions.external.enable,
|
config.completions.external.enable,
|
||||||
options.match_algorithm,
|
options,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
}
|
||||||
|
|
||||||
sort_suggestions(&String::from_utf8_lossy(prefix), commands, options)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,7 +297,7 @@ impl NuCompleter {
|
|||||||
let mut completer =
|
let mut completer =
|
||||||
OperatorCompletion::new(pipeline_element.expr.clone());
|
OperatorCompletion::new(pipeline_element.expr.clone());
|
||||||
|
|
||||||
return self.process_completion(
|
let operator_suggestion = self.process_completion(
|
||||||
&mut completer,
|
&mut completer,
|
||||||
&working_set,
|
&working_set,
|
||||||
prefix,
|
prefix,
|
||||||
@ -305,6 +305,9 @@ impl NuCompleter {
|
|||||||
fake_offset,
|
fake_offset,
|
||||||
pos,
|
pos,
|
||||||
);
|
);
|
||||||
|
if !operator_suggestion.is_empty() {
|
||||||
|
return operator_suggestion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
use super::MatchAlgorithm;
|
use super::{completion_options::NuMatcher, MatchAlgorithm};
|
||||||
use crate::{
|
use crate::completions::CompletionOptions;
|
||||||
completions::{matches, CompletionOptions},
|
|
||||||
SemanticSuggestion,
|
|
||||||
};
|
|
||||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
|
||||||
use nu_ansi_term::Style;
|
use nu_ansi_term::Style;
|
||||||
use nu_engine::env_to_string;
|
use nu_engine::env_to_string;
|
||||||
use nu_path::dots::expand_ndots;
|
use nu_path::dots::expand_ndots;
|
||||||
use nu_path::{expand_to_real_path, home_dir};
|
use nu_path::{expand_to_real_path, home_dir};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
CompletionSort, Span,
|
Span,
|
||||||
};
|
};
|
||||||
use nu_utils::get_ls_colors;
|
use nu_utils::get_ls_colors;
|
||||||
|
use nu_utils::IgnoreCaseExt;
|
||||||
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct PathBuiltFromString {
|
pub struct PathBuiltFromString {
|
||||||
|
cwd: PathBuf,
|
||||||
parts: Vec<String>,
|
parts: Vec<String>,
|
||||||
isdir: bool,
|
isdir: bool,
|
||||||
}
|
}
|
||||||
@ -30,35 +28,41 @@ pub struct PathBuiltFromString {
|
|||||||
/// want_directory: Whether we want only directories as completion matches.
|
/// want_directory: Whether we want only directories as completion matches.
|
||||||
/// Some commands like `cd` can only be run on directories whereas others
|
/// Some commands like `cd` can only be run on directories whereas others
|
||||||
/// like `ls` can be run on regular files as well.
|
/// like `ls` can be run on regular files as well.
|
||||||
pub fn complete_rec(
|
fn complete_rec(
|
||||||
partial: &[&str],
|
partial: &[&str],
|
||||||
built: &PathBuiltFromString,
|
built_paths: &[PathBuiltFromString],
|
||||||
cwd: &Path,
|
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
want_directory: bool,
|
want_directory: bool,
|
||||||
isdir: bool,
|
isdir: bool,
|
||||||
) -> Vec<PathBuiltFromString> {
|
) -> Vec<PathBuiltFromString> {
|
||||||
let mut completions = vec![];
|
|
||||||
|
|
||||||
if let Some((&base, rest)) = partial.split_first() {
|
if let Some((&base, rest)) = partial.split_first() {
|
||||||
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
||||||
|
let built_paths: Vec<_> = built_paths
|
||||||
|
.iter()
|
||||||
|
.map(|built| {
|
||||||
let mut built = built.clone();
|
let mut built = built.clone();
|
||||||
built.parts.push(base.to_string());
|
built.parts.push(base.to_string());
|
||||||
built.isdir = true;
|
built.isdir = true;
|
||||||
return complete_rec(rest, &built, cwd, options, want_directory, isdir);
|
built
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return complete_rec(rest, &built_paths, options, want_directory, isdir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut built_path = cwd.to_path_buf();
|
let prefix = partial.first().unwrap_or(&"");
|
||||||
|
let mut matcher = NuMatcher::new(prefix, options.clone());
|
||||||
|
|
||||||
|
for built in built_paths {
|
||||||
|
let mut path = built.cwd.clone();
|
||||||
for part in &built.parts {
|
for part in &built.parts {
|
||||||
built_path.push(part);
|
path.push(part);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(result) = built_path.read_dir() else {
|
let Ok(result) = path.read_dir() else {
|
||||||
return completions;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
for entry in result.filter_map(|e| e.ok()) {
|
for entry in result.filter_map(|e| e.ok()) {
|
||||||
let entry_name = entry.file_name().to_string_lossy().into_owned();
|
let entry_name = entry.file_name().to_string_lossy().into_owned();
|
||||||
let entry_isdir = entry.path().is_dir();
|
let entry_isdir = entry.path().is_dir();
|
||||||
@ -67,17 +71,15 @@ pub fn complete_rec(
|
|||||||
built.isdir = entry_isdir;
|
built.isdir = entry_isdir;
|
||||||
|
|
||||||
if !want_directory || entry_isdir {
|
if !want_directory || entry_isdir {
|
||||||
entries.push((entry_name, built));
|
matcher.add(entry_name.clone(), (entry_name, built));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefix = partial.first().unwrap_or(&"");
|
let mut completions = vec![];
|
||||||
let sorted_entries = sort_completions(prefix, entries, options, |(entry, _)| entry);
|
for (entry_name, built) in matcher.results() {
|
||||||
|
|
||||||
for (entry_name, built) in sorted_entries {
|
|
||||||
match partial.split_first() {
|
match partial.split_first() {
|
||||||
Some((base, rest)) => {
|
Some((base, rest)) => {
|
||||||
if matches(base, &entry_name, options) {
|
|
||||||
// We use `isdir` to confirm that the current component has
|
// We use `isdir` to confirm that the current component has
|
||||||
// at least one next component or a slash.
|
// at least one next component or a slash.
|
||||||
// Serves as confirmation to ignore longer completions for
|
// Serves as confirmation to ignore longer completions for
|
||||||
@ -85,8 +87,7 @@ pub fn complete_rec(
|
|||||||
if !rest.is_empty() || isdir {
|
if !rest.is_empty() || isdir {
|
||||||
completions.extend(complete_rec(
|
completions.extend(complete_rec(
|
||||||
rest,
|
rest,
|
||||||
&built,
|
&[built],
|
||||||
cwd,
|
|
||||||
options,
|
options,
|
||||||
want_directory,
|
want_directory,
|
||||||
isdir,
|
isdir,
|
||||||
@ -94,14 +95,19 @@ pub fn complete_rec(
|
|||||||
} else {
|
} else {
|
||||||
completions.push(built);
|
completions.push(built);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if entry_name.eq(base)
|
// For https://github.com/nushell/nushell/issues/13204
|
||||||
&& matches!(options.match_algorithm, MatchAlgorithm::Prefix)
|
if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
|
||||||
&& isdir
|
let exact_match = if options.case_sensitive {
|
||||||
{
|
entry_name.eq(base)
|
||||||
|
} else {
|
||||||
|
entry_name.to_folded_case().eq(&base.to_folded_case())
|
||||||
|
};
|
||||||
|
if exact_match {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
completions.push(built);
|
completions.push(built);
|
||||||
}
|
}
|
||||||
@ -147,15 +153,25 @@ fn surround_remove(partial: &str) -> String {
|
|||||||
partial.to_string()
|
partial.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FileSuggestion {
|
||||||
|
pub span: nu_protocol::Span,
|
||||||
|
pub path: String,
|
||||||
|
pub style: Option<Style>,
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Parameters
|
||||||
|
/// * `cwds` - A list of directories in which to search. The only reason this isn't a single string
|
||||||
|
/// is because dotnu_completions searches in multiple directories at once
|
||||||
pub fn complete_item(
|
pub fn complete_item(
|
||||||
want_directory: bool,
|
want_directory: bool,
|
||||||
span: nu_protocol::Span,
|
span: nu_protocol::Span,
|
||||||
partial: &str,
|
partial: &str,
|
||||||
cwd: &str,
|
cwds: &[impl AsRef<str>],
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
) -> Vec<FileSuggestion> {
|
||||||
let cleaned_partial = surround_remove(partial);
|
let cleaned_partial = surround_remove(partial);
|
||||||
let isdir = cleaned_partial.ends_with(is_separator);
|
let isdir = cleaned_partial.ends_with(is_separator);
|
||||||
let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
|
let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
|
||||||
@ -175,7 +191,10 @@ pub fn complete_item(
|
|||||||
partial.push_str(&format!("{path_separator}."));
|
partial.push_str(&format!("{path_separator}."));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cwd_pathbuf = Path::new(cwd).to_path_buf();
|
let cwd_pathbufs: Vec<_> = cwds
|
||||||
|
.iter()
|
||||||
|
.map(|cwd| Path::new(cwd.as_ref()).to_path_buf())
|
||||||
|
.collect();
|
||||||
let ls_colors = (engine_state.config.completions.use_ls_colors
|
let ls_colors = (engine_state.config.completions.use_ls_colors
|
||||||
&& engine_state.config.use_ansi_coloring)
|
&& engine_state.config.use_ansi_coloring)
|
||||||
.then(|| {
|
.then(|| {
|
||||||
@ -186,7 +205,7 @@ pub fn complete_item(
|
|||||||
get_ls_colors(ls_colors_env_str)
|
get_ls_colors(ls_colors_env_str)
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut cwd = cwd_pathbuf.clone();
|
let mut cwds = cwd_pathbufs.clone();
|
||||||
let mut prefix_len = 0;
|
let mut prefix_len = 0;
|
||||||
let mut original_cwd = OriginalCwd::None;
|
let mut original_cwd = OriginalCwd::None;
|
||||||
|
|
||||||
@ -194,19 +213,21 @@ pub fn complete_item(
|
|||||||
match components.peek().cloned() {
|
match components.peek().cloned() {
|
||||||
Some(c @ Component::Prefix(..)) => {
|
Some(c @ Component::Prefix(..)) => {
|
||||||
// windows only by definition
|
// windows only by definition
|
||||||
cwd = [c, Component::RootDir].iter().collect();
|
cwds = vec![[c, Component::RootDir].iter().collect()];
|
||||||
prefix_len = c.as_os_str().len();
|
prefix_len = c.as_os_str().len();
|
||||||
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
|
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
|
||||||
}
|
}
|
||||||
Some(c @ Component::RootDir) => {
|
Some(c @ Component::RootDir) => {
|
||||||
// This is kind of a hack. When joining an empty string with the rest,
|
// This is kind of a hack. When joining an empty string with the rest,
|
||||||
// we add the slash automagically
|
// we add the slash automagically
|
||||||
cwd = PathBuf::from(c.as_os_str());
|
cwds = vec![PathBuf::from(c.as_os_str())];
|
||||||
prefix_len = 1;
|
prefix_len = 1;
|
||||||
original_cwd = OriginalCwd::Prefix(String::new());
|
original_cwd = OriginalCwd::Prefix(String::new());
|
||||||
}
|
}
|
||||||
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
||||||
cwd = home_dir().map(Into::into).unwrap_or(cwd_pathbuf);
|
cwds = home_dir()
|
||||||
|
.map(|dir| vec![dir.into()])
|
||||||
|
.unwrap_or(cwd_pathbufs);
|
||||||
prefix_len = 1;
|
prefix_len = 1;
|
||||||
original_cwd = OriginalCwd::Home;
|
original_cwd = OriginalCwd::Home;
|
||||||
}
|
}
|
||||||
@ -223,8 +244,14 @@ pub fn complete_item(
|
|||||||
|
|
||||||
complete_rec(
|
complete_rec(
|
||||||
partial.as_slice(),
|
partial.as_slice(),
|
||||||
&PathBuiltFromString::default(),
|
&cwds
|
||||||
&cwd,
|
.into_iter()
|
||||||
|
.map(|cwd| PathBuiltFromString {
|
||||||
|
cwd,
|
||||||
|
parts: Vec::new(),
|
||||||
|
isdir: false,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
options,
|
options,
|
||||||
want_directory,
|
want_directory,
|
||||||
isdir,
|
isdir,
|
||||||
@ -234,6 +261,7 @@ pub fn complete_item(
|
|||||||
if should_collapse_dots {
|
if should_collapse_dots {
|
||||||
p = collapse_ndots(p);
|
p = collapse_ndots(p);
|
||||||
}
|
}
|
||||||
|
let cwd = p.cwd.clone();
|
||||||
let path = original_cwd.apply(p, path_separator);
|
let path = original_cwd.apply(p, path_separator);
|
||||||
let style = ls_colors.as_ref().map(|lsc| {
|
let style = ls_colors.as_ref().map(|lsc| {
|
||||||
lsc.style_for_path_with_metadata(
|
lsc.style_for_path_with_metadata(
|
||||||
@ -245,7 +273,12 @@ pub fn complete_item(
|
|||||||
.map(lscolors::Style::to_nu_ansi_term_style)
|
.map(lscolors::Style::to_nu_ansi_term_style)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
});
|
});
|
||||||
(span, escape_path(path, want_directory), style)
|
FileSuggestion {
|
||||||
|
span,
|
||||||
|
path: escape_path(path, want_directory),
|
||||||
|
style,
|
||||||
|
cwd,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@ -310,45 +343,6 @@ pub fn adjust_if_intermediate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to sort suggestions using [`sort_completions`]
|
|
||||||
pub fn sort_suggestions(
|
|
||||||
prefix: &str,
|
|
||||||
items: Vec<SemanticSuggestion>,
|
|
||||||
options: &CompletionOptions,
|
|
||||||
) -> Vec<SemanticSuggestion> {
|
|
||||||
sort_completions(prefix, items, options, |it| &it.suggestion.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Arguments
|
|
||||||
/// * `prefix` - What the user's typed, for sorting by fuzzy matcher score
|
|
||||||
pub fn sort_completions<T>(
|
|
||||||
prefix: &str,
|
|
||||||
mut items: Vec<T>,
|
|
||||||
options: &CompletionOptions,
|
|
||||||
get_value: fn(&T) -> &str,
|
|
||||||
) -> Vec<T> {
|
|
||||||
// Sort items
|
|
||||||
if options.sort == CompletionSort::Smart && options.match_algorithm == MatchAlgorithm::Fuzzy {
|
|
||||||
let mut matcher = SkimMatcherV2::default();
|
|
||||||
if options.case_sensitive {
|
|
||||||
matcher = matcher.respect_case();
|
|
||||||
} else {
|
|
||||||
matcher = matcher.ignore_case();
|
|
||||||
};
|
|
||||||
items.sort_unstable_by(|a, b| {
|
|
||||||
let a_str = get_value(a);
|
|
||||||
let b_str = get_value(b);
|
|
||||||
let a_score = matcher.fuzzy_match(a_str, prefix).unwrap_or_default();
|
|
||||||
let b_score = matcher.fuzzy_match(b_str, prefix).unwrap_or_default();
|
|
||||||
b_score.cmp(&a_score).then(a_str.cmp(b_str))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
items.sort_unstable_by(|a, b| get_value(a).cmp(get_value(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collapse multiple ".." components into n-dots.
|
/// Collapse multiple ".." components into n-dots.
|
||||||
///
|
///
|
||||||
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
|
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
|
||||||
@ -359,6 +353,7 @@ fn collapse_ndots(path: PathBuiltFromString) -> PathBuiltFromString {
|
|||||||
let mut result = PathBuiltFromString {
|
let mut result = PathBuiltFromString {
|
||||||
parts: Vec::with_capacity(path.parts.len()),
|
parts: Vec::with_capacity(path.parts.len()),
|
||||||
isdir: path.isdir,
|
isdir: path.isdir,
|
||||||
|
cwd: path.cwd,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut dot_count = 0;
|
let mut dot_count = 0;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||||
use nu_parser::trim_quotes_str;
|
use nu_parser::trim_quotes_str;
|
||||||
use nu_protocol::{CompletionAlgorithm, CompletionSort};
|
use nu_protocol::{CompletionAlgorithm, CompletionSort};
|
||||||
use std::fmt::Display;
|
use nu_utils::IgnoreCaseExt;
|
||||||
|
use std::{borrow::Cow, fmt::Display};
|
||||||
|
|
||||||
|
use super::SemanticSuggestion;
|
||||||
|
|
||||||
/// Describes how suggestions should be matched.
|
/// Describes how suggestions should be matched.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
@ -19,32 +22,153 @@ pub enum MatchAlgorithm {
|
|||||||
Fuzzy,
|
Fuzzy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MatchAlgorithm {
|
pub struct NuMatcher<T> {
|
||||||
/// Returns whether the `needle` search text matches the given `haystack`.
|
options: CompletionOptions,
|
||||||
pub fn matches_str(&self, haystack: &str, needle: &str) -> bool {
|
needle: String,
|
||||||
|
state: State<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State<T> {
|
||||||
|
Prefix {
|
||||||
|
/// Holds (haystack, item)
|
||||||
|
items: Vec<(String, T)>,
|
||||||
|
},
|
||||||
|
Fuzzy {
|
||||||
|
matcher: Box<SkimMatcherV2>,
|
||||||
|
/// Holds (haystack, item, score)
|
||||||
|
items: Vec<(String, T, i64)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filters and sorts suggestions
|
||||||
|
impl<T> NuMatcher<T> {
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `needle` - The text to search for
|
||||||
|
pub fn new(needle: impl AsRef<str>, options: CompletionOptions) -> NuMatcher<T> {
|
||||||
|
let orig_needle = trim_quotes_str(needle.as_ref());
|
||||||
|
let lowercase_needle = if options.case_sensitive {
|
||||||
|
orig_needle.to_owned()
|
||||||
|
} else {
|
||||||
|
orig_needle.to_folded_case()
|
||||||
|
};
|
||||||
|
match options.match_algorithm {
|
||||||
|
MatchAlgorithm::Prefix => NuMatcher {
|
||||||
|
options,
|
||||||
|
needle: lowercase_needle,
|
||||||
|
state: State::Prefix { items: Vec::new() },
|
||||||
|
},
|
||||||
|
MatchAlgorithm::Fuzzy => {
|
||||||
|
let mut matcher = SkimMatcherV2::default();
|
||||||
|
if options.case_sensitive {
|
||||||
|
matcher = matcher.respect_case();
|
||||||
|
} else {
|
||||||
|
matcher = matcher.ignore_case();
|
||||||
|
};
|
||||||
|
NuMatcher {
|
||||||
|
options,
|
||||||
|
needle: orig_needle.to_owned(),
|
||||||
|
state: State::Fuzzy {
|
||||||
|
matcher: Box::new(matcher),
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether or not the haystack matches the needle. If it does, `item` is added
|
||||||
|
/// to the list of matches (if given).
|
||||||
|
///
|
||||||
|
/// Helper to avoid code duplication between [NuMatcher::add] and [NuMatcher::matches].
|
||||||
|
fn matches_aux(&mut self, haystack: &str, item: Option<T>) -> bool {
|
||||||
let haystack = trim_quotes_str(haystack);
|
let haystack = trim_quotes_str(haystack);
|
||||||
let needle = trim_quotes_str(needle);
|
match &mut self.state {
|
||||||
match *self {
|
State::Prefix { items } => {
|
||||||
MatchAlgorithm::Prefix => haystack.starts_with(needle),
|
let haystack_folded = if self.options.case_sensitive {
|
||||||
MatchAlgorithm::Fuzzy => {
|
Cow::Borrowed(haystack)
|
||||||
let matcher = SkimMatcherV2::default();
|
} else {
|
||||||
matcher.fuzzy_match(haystack, needle).is_some()
|
Cow::Owned(haystack.to_folded_case())
|
||||||
|
};
|
||||||
|
let matches = if self.options.positional {
|
||||||
|
haystack_folded.starts_with(self.needle.as_str())
|
||||||
|
} else {
|
||||||
|
haystack_folded.contains(self.needle.as_str())
|
||||||
|
};
|
||||||
|
if matches {
|
||||||
|
if let Some(item) = item {
|
||||||
|
items.push((haystack.to_string(), item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches
|
||||||
|
}
|
||||||
|
State::Fuzzy { items, matcher } => {
|
||||||
|
let Some(score) = matcher.fuzzy_match(haystack, &self.needle) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
items.push((haystack.to_string(), item, score));
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the `needle` search text matches the given `haystack`.
|
/// Add the given item if the given haystack matches the needle.
|
||||||
pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool {
|
///
|
||||||
match *self {
|
/// Returns whether the item was added.
|
||||||
MatchAlgorithm::Prefix => haystack.starts_with(needle),
|
pub fn add(&mut self, haystack: impl AsRef<str>, item: T) -> bool {
|
||||||
MatchAlgorithm::Fuzzy => {
|
self.matches_aux(haystack.as_ref(), Some(item))
|
||||||
let haystack_str = String::from_utf8_lossy(haystack);
|
}
|
||||||
let needle_str = String::from_utf8_lossy(needle);
|
|
||||||
|
|
||||||
let matcher = SkimMatcherV2::default();
|
/// Returns whether the haystack matches the needle.
|
||||||
matcher.fuzzy_match(&haystack_str, &needle_str).is_some()
|
pub fn matches(&mut self, haystack: &str) -> bool {
|
||||||
|
self.matches_aux(haystack, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all the items that matched (sorted)
|
||||||
|
pub fn results(self) -> Vec<T> {
|
||||||
|
match self.state {
|
||||||
|
State::Prefix { mut items, .. } => {
|
||||||
|
items.sort_by(|(haystack1, _), (haystack2, _)| {
|
||||||
|
let cmp_sensitive = haystack1.cmp(haystack2);
|
||||||
|
if self.options.case_sensitive {
|
||||||
|
cmp_sensitive
|
||||||
|
} else {
|
||||||
|
haystack1
|
||||||
|
.to_folded_case()
|
||||||
|
.cmp(&haystack2.to_folded_case())
|
||||||
|
.then(cmp_sensitive)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
items.into_iter().map(|(_, item)| item).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
State::Fuzzy { mut items, .. } => {
|
||||||
|
match self.options.sort {
|
||||||
|
CompletionSort::Alphabetical => {
|
||||||
|
items.sort_by(|(haystack1, _, _), (haystack2, _, _)| {
|
||||||
|
haystack1.cmp(haystack2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CompletionSort::Smart => {
|
||||||
|
items.sort_by(|(haystack1, _, score1), (haystack2, _, score2)| {
|
||||||
|
score2.cmp(score1).then(haystack1.cmp(haystack2))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, item, _)| item)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NuMatcher<SemanticSuggestion> {
|
||||||
|
pub fn add_semantic_suggestion(&mut self, sugg: SemanticSuggestion) -> bool {
|
||||||
|
let value = sugg.suggestion.value.to_string();
|
||||||
|
self.add(value, sugg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,35 +229,49 @@ impl Default for CompletionOptions {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::MatchAlgorithm;
|
use rstest::rstest;
|
||||||
|
|
||||||
#[test]
|
use super::{CompletionOptions, MatchAlgorithm, NuMatcher};
|
||||||
fn match_algorithm_prefix() {
|
|
||||||
let algorithm = MatchAlgorithm::Prefix;
|
|
||||||
|
|
||||||
assert!(algorithm.matches_str("example text", ""));
|
#[rstest]
|
||||||
assert!(algorithm.matches_str("example text", "examp"));
|
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
||||||
assert!(!algorithm.matches_str("example text", "text"));
|
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
||||||
|
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
|
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
|
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
||||||
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
|
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "mplxt", true)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "mpp", false)]
|
||||||
|
fn match_algorithm_simple(
|
||||||
|
#[case] match_algorithm: MatchAlgorithm,
|
||||||
|
#[case] haystack: &str,
|
||||||
|
#[case] needle: &str,
|
||||||
|
#[case] should_match: bool,
|
||||||
|
) {
|
||||||
|
let options = CompletionOptions {
|
||||||
|
match_algorithm,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut matcher = NuMatcher::new(needle, options);
|
||||||
|
matcher.add(haystack, haystack);
|
||||||
|
if should_match {
|
||||||
|
assert_eq!(vec![haystack], matcher.results());
|
||||||
|
} else {
|
||||||
|
assert_ne!(vec![haystack], matcher.results());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn match_algorithm_fuzzy() {
|
fn match_algorithm_fuzzy_sort_score() {
|
||||||
let algorithm = MatchAlgorithm::Fuzzy;
|
let options = CompletionOptions {
|
||||||
|
match_algorithm: MatchAlgorithm::Fuzzy,
|
||||||
assert!(algorithm.matches_str("example text", ""));
|
..Default::default()
|
||||||
assert!(algorithm.matches_str("example text", "examp"));
|
};
|
||||||
assert!(algorithm.matches_str("example text", "ext"));
|
let mut matcher = NuMatcher::new("fob", options);
|
||||||
assert!(algorithm.matches_str("example text", "mplxt"));
|
for item in ["foo/bar", "fob", "foo bar"] {
|
||||||
assert!(!algorithm.matches_str("example text", "mpp"));
|
matcher.add(item, item);
|
||||||
|
}
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
|
// Sort by score, then in alphabetical order
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
|
assert_eq!(vec!["fob", "foo bar", "foo/bar"], matcher.results());
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
|
|
||||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 3]));
|
|
||||||
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 2]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
use crate::completions::{
|
use crate::completions::{
|
||||||
completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm,
|
completer::map_value_completions, Completer, CompletionOptions, SemanticSuggestion,
|
||||||
SemanticSuggestion,
|
|
||||||
};
|
};
|
||||||
use nu_engine::eval_call;
|
use nu_engine::eval_call;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Argument, Call, Expr, Expression},
|
ast::{Argument, Call, Expr, Expression},
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
CompletionSort, DeclId, PipelineData, Span, Type, Value,
|
DeclId, PipelineData, Span, Type, Value,
|
||||||
};
|
};
|
||||||
use nu_utils::IgnoreCaseExt;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::completion_common::sort_suggestions;
|
use super::completion_options::NuMatcher;
|
||||||
|
|
||||||
pub struct CustomCompletion {
|
pub struct CustomCompletion {
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
@ -69,6 +67,7 @@ impl Completer for CustomCompletion {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut custom_completion_options = None;
|
let mut custom_completion_options = None;
|
||||||
|
let mut should_sort = true;
|
||||||
|
|
||||||
// Parse result
|
// Parse result
|
||||||
let suggestions = result
|
let suggestions = result
|
||||||
@ -86,10 +85,9 @@ impl Completer for CustomCompletion {
|
|||||||
let options = val.get("options");
|
let options = val.get("options");
|
||||||
|
|
||||||
if let Some(Value::Record { val: options, .. }) = &options {
|
if let Some(Value::Record { val: options, .. }) = &options {
|
||||||
let should_sort = options
|
if let Some(sort) = options.get("sort").and_then(|val| val.as_bool().ok()) {
|
||||||
.get("sort")
|
should_sort = sort;
|
||||||
.and_then(|val| val.as_bool().ok())
|
}
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
custom_completion_options = Some(CompletionOptions {
|
custom_completion_options = Some(CompletionOptions {
|
||||||
case_sensitive: options
|
case_sensitive: options
|
||||||
@ -99,20 +97,16 @@ impl Completer for CustomCompletion {
|
|||||||
positional: options
|
positional: options
|
||||||
.get("positional")
|
.get("positional")
|
||||||
.and_then(|val| val.as_bool().ok())
|
.and_then(|val| val.as_bool().ok())
|
||||||
.unwrap_or(true),
|
.unwrap_or(completion_options.positional),
|
||||||
match_algorithm: match options.get("completion_algorithm") {
|
match_algorithm: match options.get("completion_algorithm") {
|
||||||
Some(option) => option
|
Some(option) => option
|
||||||
.coerce_string()
|
.coerce_string()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|option| option.try_into().ok())
|
.and_then(|option| option.try_into().ok())
|
||||||
.unwrap_or(MatchAlgorithm::Prefix),
|
.unwrap_or(completion_options.match_algorithm),
|
||||||
None => completion_options.match_algorithm,
|
None => completion_options.match_algorithm,
|
||||||
},
|
},
|
||||||
sort: if should_sort {
|
sort: completion_options.sort,
|
||||||
CompletionSort::Alphabetical
|
|
||||||
} else {
|
|
||||||
CompletionSort::Smart
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,41 +117,19 @@ impl Completer for CustomCompletion {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let options = custom_completion_options
|
let options = custom_completion_options.unwrap_or(completion_options.clone());
|
||||||
.as_ref()
|
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options);
|
||||||
.unwrap_or(completion_options);
|
|
||||||
let suggestions = filter(prefix, suggestions, options);
|
|
||||||
sort_suggestions(&String::from_utf8_lossy(prefix), suggestions, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn filter(
|
if should_sort {
|
||||||
prefix: &[u8],
|
for sugg in suggestions {
|
||||||
items: Vec<SemanticSuggestion>,
|
matcher.add_semantic_suggestion(sugg);
|
||||||
options: &CompletionOptions,
|
}
|
||||||
) -> Vec<SemanticSuggestion> {
|
matcher.results()
|
||||||
items
|
|
||||||
.into_iter()
|
|
||||||
.filter(|it| match options.match_algorithm {
|
|
||||||
MatchAlgorithm::Prefix => match (options.case_sensitive, options.positional) {
|
|
||||||
(true, true) => it.suggestion.value.as_bytes().starts_with(prefix),
|
|
||||||
(true, false) => it
|
|
||||||
.suggestion
|
|
||||||
.value
|
|
||||||
.contains(std::str::from_utf8(prefix).unwrap_or("")),
|
|
||||||
(false, positional) => {
|
|
||||||
let value = it.suggestion.value.to_folded_case();
|
|
||||||
let prefix = std::str::from_utf8(prefix).unwrap_or("").to_folded_case();
|
|
||||||
if positional {
|
|
||||||
value.starts_with(&prefix)
|
|
||||||
} else {
|
} else {
|
||||||
value.contains(&prefix)
|
suggestions
|
||||||
}
|
.into_iter()
|
||||||
}
|
.filter(|sugg| matcher.matches(&sugg.suggestion.value))
|
||||||
},
|
|
||||||
MatchAlgorithm::Fuzzy => options
|
|
||||||
.match_algorithm
|
|
||||||
.matches_u8(it.suggestion.value.as_bytes(), prefix),
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,6 @@ use crate::completions::{
|
|||||||
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
||||||
Completer, CompletionOptions,
|
Completer, CompletionOptions,
|
||||||
};
|
};
|
||||||
use nu_ansi_term::Style;
|
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
Span,
|
Span,
|
||||||
@ -10,7 +9,7 @@ use nu_protocol::{
|
|||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::SemanticSuggestion;
|
use super::{completion_common::FileSuggestion, SemanticSuggestion};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct DirectoryCompletion {}
|
pub struct DirectoryCompletion {}
|
||||||
@ -47,11 +46,11 @@ impl Completer for DirectoryCompletion {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(move |x| SemanticSuggestion {
|
.map(move |x| SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: x.1,
|
value: x.path,
|
||||||
style: x.2,
|
style: x.style,
|
||||||
span: reedline::Span {
|
span: reedline::Span {
|
||||||
start: x.0.start - offset,
|
start: x.span.start - offset,
|
||||||
end: x.0.end - offset,
|
end: x.span.end - offset,
|
||||||
},
|
},
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
},
|
},
|
||||||
@ -92,6 +91,6 @@ pub fn directory_completion(
|
|||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
) -> Vec<FileSuggestion> {
|
||||||
complete_item(true, span, partial, cwd, options, engine_state, stack)
|
complete_item(true, span, partial, &[cwd], options, engine_state, stack)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use nu_protocol::{
|
|||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
|
use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
|
||||||
|
|
||||||
use super::{completion_common::sort_suggestions, SemanticSuggestion};
|
use super::SemanticSuggestion;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct DotNuCompletion {}
|
pub struct DotNuCompletion {}
|
||||||
@ -87,13 +87,11 @@ impl Completer for DotNuCompletion {
|
|||||||
|
|
||||||
// Fetch the files filtering the ones that ends with .nu
|
// Fetch the files filtering the ones that ends with .nu
|
||||||
// and transform them into suggestions
|
// and transform them into suggestions
|
||||||
let output: Vec<SemanticSuggestion> = search_dirs
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|search_dir| {
|
|
||||||
let completions = file_path_completion(
|
let completions = file_path_completion(
|
||||||
span,
|
span,
|
||||||
&partial,
|
&partial,
|
||||||
&search_dir,
|
&search_dirs.iter().map(|d| d.as_str()).collect::<Vec<_>>(),
|
||||||
options,
|
options,
|
||||||
working_set.permanent_state,
|
working_set.permanent_state,
|
||||||
stack,
|
stack,
|
||||||
@ -103,23 +101,23 @@ impl Completer for DotNuCompletion {
|
|||||||
.filter(move |it| {
|
.filter(move |it| {
|
||||||
// Different base dir, so we list the .nu files or folders
|
// Different base dir, so we list the .nu files or folders
|
||||||
if !is_current_folder {
|
if !is_current_folder {
|
||||||
it.1.ends_with(".nu") || it.1.ends_with(SEP)
|
it.path.ends_with(".nu") || it.path.ends_with(SEP)
|
||||||
} else {
|
} else {
|
||||||
// Lib dirs, so we filter only the .nu files or directory modules
|
// Lib dirs, so we filter only the .nu files or directory modules
|
||||||
if it.1.ends_with(SEP) {
|
if it.path.ends_with(SEP) {
|
||||||
Path::new(&search_dir).join(&it.1).join("mod.nu").exists()
|
Path::new(&it.cwd).join(&it.path).join("mod.nu").exists()
|
||||||
} else {
|
} else {
|
||||||
it.1.ends_with(".nu")
|
it.path.ends_with(".nu")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(move |x| SemanticSuggestion {
|
.map(move |x| SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: x.1,
|
value: x.path,
|
||||||
style: x.2,
|
style: x.style,
|
||||||
span: reedline::Span {
|
span: reedline::Span {
|
||||||
start: x.0.start - offset,
|
start: x.span.start - offset,
|
||||||
end: x.0.end - offset,
|
end: x.span.end - offset,
|
||||||
},
|
},
|
||||||
append_whitespace: true,
|
append_whitespace: true,
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
@ -127,9 +125,6 @@ impl Completer for DotNuCompletion {
|
|||||||
// TODO????
|
// TODO????
|
||||||
kind: None,
|
kind: None,
|
||||||
})
|
})
|
||||||
})
|
.collect::<Vec<_>>()
|
||||||
.collect();
|
|
||||||
|
|
||||||
sort_suggestions(&prefix_str, output, options)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,14 @@ use crate::completions::{
|
|||||||
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
||||||
Completer, CompletionOptions,
|
Completer, CompletionOptions,
|
||||||
};
|
};
|
||||||
use nu_ansi_term::Style;
|
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
Span,
|
Span,
|
||||||
};
|
};
|
||||||
use nu_utils::IgnoreCaseExt;
|
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::SemanticSuggestion;
|
use super::{completion_common::FileSuggestion, SemanticSuggestion};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct FileCompletion {}
|
pub struct FileCompletion {}
|
||||||
@ -44,7 +42,7 @@ impl Completer for FileCompletion {
|
|||||||
readjusted,
|
readjusted,
|
||||||
span,
|
span,
|
||||||
&prefix,
|
&prefix,
|
||||||
&working_set.permanent_state.current_work_dir(),
|
&[&working_set.permanent_state.current_work_dir()],
|
||||||
options,
|
options,
|
||||||
working_set.permanent_state,
|
working_set.permanent_state,
|
||||||
stack,
|
stack,
|
||||||
@ -52,11 +50,11 @@ impl Completer for FileCompletion {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(move |x| SemanticSuggestion {
|
.map(move |x| SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: x.1,
|
value: x.path,
|
||||||
style: x.2,
|
style: x.style,
|
||||||
span: reedline::Span {
|
span: reedline::Span {
|
||||||
start: x.0.start - offset,
|
start: x.span.start - offset,
|
||||||
end: x.0.end - offset,
|
end: x.span.end - offset,
|
||||||
},
|
},
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
},
|
},
|
||||||
@ -95,21 +93,10 @@ impl Completer for FileCompletion {
|
|||||||
pub fn file_path_completion(
|
pub fn file_path_completion(
|
||||||
span: nu_protocol::Span,
|
span: nu_protocol::Span,
|
||||||
partial: &str,
|
partial: &str,
|
||||||
cwd: &str,
|
cwds: &[impl AsRef<str>],
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
) -> Vec<FileSuggestion> {
|
||||||
complete_item(false, span, partial, cwd, options, engine_state, stack)
|
complete_item(false, span, partial, cwds, options, engine_state, stack)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
|
|
||||||
// Check for case sensitive
|
|
||||||
if !options.case_sensitive {
|
|
||||||
return options
|
|
||||||
.match_algorithm
|
|
||||||
.matches_str(&from.to_folded_case(), &partial.to_folded_case());
|
|
||||||
}
|
|
||||||
|
|
||||||
options.match_algorithm.matches_str(from, partial)
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::completions::{completion_common::sort_suggestions, Completer, CompletionOptions};
|
use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Expr, Expression},
|
ast::{Expr, Expression},
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
@ -35,7 +35,7 @@ impl Completer for FlagCompletion {
|
|||||||
let decl = working_set.get_decl(call.decl_id);
|
let decl = working_set.get_decl(call.decl_id);
|
||||||
let sig = decl.signature();
|
let sig = decl.signature();
|
||||||
|
|
||||||
let mut output = vec![];
|
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), options.clone());
|
||||||
|
|
||||||
for named in &sig.named {
|
for named in &sig.named {
|
||||||
let flag_desc = &named.desc;
|
let flag_desc = &named.desc;
|
||||||
@ -44,8 +44,7 @@ impl Completer for FlagCompletion {
|
|||||||
short.encode_utf8(&mut named);
|
short.encode_utf8(&mut named);
|
||||||
named.insert(0, b'-');
|
named.insert(0, b'-');
|
||||||
|
|
||||||
if options.match_algorithm.matches_u8(&named, prefix) {
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
output.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(&named).to_string(),
|
value: String::from_utf8_lossy(&named).to_string(),
|
||||||
description: Some(flag_desc.to_string()),
|
description: Some(flag_desc.to_string()),
|
||||||
@ -60,7 +59,6 @@ impl Completer for FlagCompletion {
|
|||||||
kind: None,
|
kind: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if named.long.is_empty() {
|
if named.long.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
@ -70,8 +68,7 @@ impl Completer for FlagCompletion {
|
|||||||
named.insert(0, b'-');
|
named.insert(0, b'-');
|
||||||
named.insert(0, b'-');
|
named.insert(0, b'-');
|
||||||
|
|
||||||
if options.match_algorithm.matches_u8(&named, prefix) {
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
output.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(&named).to_string(),
|
value: String::from_utf8_lossy(&named).to_string(),
|
||||||
description: Some(flag_desc.to_string()),
|
description: Some(flag_desc.to_string()),
|
||||||
@ -86,9 +83,8 @@ impl Completer for FlagCompletion {
|
|||||||
kind: None,
|
kind: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return sort_suggestions(&String::from_utf8_lossy(prefix), output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
|
|
||||||
vec![]
|
vec![]
|
||||||
|
@ -18,7 +18,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
|
|||||||
pub use custom_completions::CustomCompletion;
|
pub use custom_completions::CustomCompletion;
|
||||||
pub use directory_completions::DirectoryCompletion;
|
pub use directory_completions::DirectoryCompletion;
|
||||||
pub use dotnu_completions::DotNuCompletion;
|
pub use dotnu_completions::DotNuCompletion;
|
||||||
pub use file_completions::{file_path_completion, matches, FileCompletion};
|
pub use file_completions::{file_path_completion, FileCompletion};
|
||||||
pub use flag_completions::FlagCompletion;
|
pub use flag_completions::FlagCompletion;
|
||||||
pub use operator_completions::OperatorCompletion;
|
pub use operator_completions::OperatorCompletion;
|
||||||
pub use variable_completions::VariableCompletion;
|
pub use variable_completions::VariableCompletion;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::completions::{
|
use crate::completions::{
|
||||||
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
|
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||||
};
|
};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Expr, Expression},
|
ast::{Expr, Expression},
|
||||||
@ -28,7 +28,7 @@ impl Completer for OperatorCompletion {
|
|||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
_pos: usize,
|
_pos: usize,
|
||||||
_options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
//Check if int, float, or string
|
//Check if int, float, or string
|
||||||
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or("");
|
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or("");
|
||||||
@ -60,17 +60,15 @@ impl Completer for OperatorCompletion {
|
|||||||
("bit-shr", "Bitwise shift right"),
|
("bit-shr", "Bitwise shift right"),
|
||||||
("in", "Is a member of (doesn't use regex)"),
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
("not-in", "Is not a member of (doesn't use regex)"),
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
(
|
|
||||||
"++",
|
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
Expr::String(_) => vec![
|
Expr::String(_) => vec![
|
||||||
("=~", "Contains regex match"),
|
("=~", "Contains regex match"),
|
||||||
|
("like", "Contains regex match"),
|
||||||
("!~", "Does not contain regex match"),
|
("!~", "Does not contain regex match"),
|
||||||
|
("not-like", "Does not contain regex match"),
|
||||||
(
|
(
|
||||||
"++",
|
"++",
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
"Concatenates two lists, two strings, or two binary values",
|
||||||
),
|
),
|
||||||
("in", "Is a member of (doesn't use regex)"),
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
("not-in", "Is not a member of (doesn't use regex)"),
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
@ -93,10 +91,6 @@ impl Completer for OperatorCompletion {
|
|||||||
("**", "Power of"),
|
("**", "Power of"),
|
||||||
("in", "Is a member of (doesn't use regex)"),
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
("not-in", "Is not a member of (doesn't use regex)"),
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
(
|
|
||||||
"++",
|
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
Expr::Bool(_) => vec![
|
Expr::Bool(_) => vec![
|
||||||
(
|
(
|
||||||
@ -111,15 +105,11 @@ impl Completer for OperatorCompletion {
|
|||||||
("not", "Negates a value or expression"),
|
("not", "Negates a value or expression"),
|
||||||
("in", "Is a member of (doesn't use regex)"),
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
("not-in", "Is not a member of (doesn't use regex)"),
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
(
|
|
||||||
"++",
|
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
Expr::FullCellPath(path) => match path.head.expr {
|
Expr::FullCellPath(path) => match path.head.expr {
|
||||||
Expr::List(_) => vec![(
|
Expr::List(_) => vec![(
|
||||||
"++",
|
"++",
|
||||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
"Concatenates two lists, two strings, or two binary values",
|
||||||
)],
|
)],
|
||||||
Expr::Var(id) => get_variable_completions(id, working_set),
|
Expr::Var(id) => get_variable_completions(id, working_set),
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
@ -127,17 +117,12 @@ impl Completer for OperatorCompletion {
|
|||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let match_algorithm = MatchAlgorithm::Prefix;
|
let mut matcher = NuMatcher::new(partial, options.clone());
|
||||||
let input_fuzzy_search =
|
for (symbol, desc) in possible_operations.into_iter() {
|
||||||
|(operator, _): &(&str, &str)| match_algorithm.matches_str(operator, partial);
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
|
||||||
possible_operations
|
|
||||||
.into_iter()
|
|
||||||
.filter(input_fuzzy_search)
|
|
||||||
.map(move |x| SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: x.0.to_string(),
|
value: symbol.to_string(),
|
||||||
description: Some(x.1.to_string()),
|
description: Some(desc.to_string()),
|
||||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
span: reedline::Span::new(span.start - offset, span.end - offset),
|
||||||
append_whitespace: true,
|
append_whitespace: true,
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
@ -145,8 +130,9 @@ impl Completer for OperatorCompletion {
|
|||||||
kind: Some(SuggestionKind::Command(
|
kind: Some(SuggestionKind::Command(
|
||||||
nu_protocol::engine::CommandType::Builtin,
|
nu_protocol::engine::CommandType::Builtin,
|
||||||
)),
|
)),
|
||||||
})
|
});
|
||||||
.collect()
|
}
|
||||||
|
matcher.results()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +149,7 @@ pub fn get_variable_completions<'a>(
|
|||||||
Type::List(_) | Type::String | Type::Binary => vec![
|
Type::List(_) | Type::String | Type::Binary => vec![
|
||||||
(
|
(
|
||||||
"++=",
|
"++=",
|
||||||
"Appends a list, a value, a string, or a binary value to a variable.",
|
"Concatenates two lists, two strings, or two binary values",
|
||||||
),
|
),
|
||||||
("=", "Assigns a value to a variable."),
|
("=", "Assigns a value to a variable."),
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
use crate::completions::{
|
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
|
||||||
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
|
|
||||||
};
|
|
||||||
use nu_engine::{column::get_columns, eval_variable};
|
use nu_engine::{column::get_columns, eval_variable};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
@ -9,7 +7,7 @@ use nu_protocol::{
|
|||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
use super::completion_common::sort_suggestions;
|
use super::completion_options::NuMatcher;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct VariableCompletion {
|
pub struct VariableCompletion {
|
||||||
@ -33,7 +31,6 @@ impl Completer for VariableCompletion {
|
|||||||
_pos: usize,
|
_pos: usize,
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
let mut output = vec![];
|
|
||||||
let builtins = ["$nu", "$in", "$env"];
|
let builtins = ["$nu", "$in", "$env"];
|
||||||
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
|
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
|
||||||
let var_id = working_set.find_variable(&self.var_context.0);
|
let var_id = working_set.find_variable(&self.var_context.0);
|
||||||
@ -43,6 +40,7 @@ impl Completer for VariableCompletion {
|
|||||||
};
|
};
|
||||||
let sublevels_count = self.var_context.1.len();
|
let sublevels_count = self.var_context.1.len();
|
||||||
let prefix_str = String::from_utf8_lossy(prefix);
|
let prefix_str = String::from_utf8_lossy(prefix);
|
||||||
|
let mut matcher = NuMatcher::new(prefix_str, options.clone());
|
||||||
|
|
||||||
// Completions for the given variable
|
// Completions for the given variable
|
||||||
if !var_str.is_empty() {
|
if !var_str.is_empty() {
|
||||||
@ -63,26 +61,15 @@ impl Completer for VariableCompletion {
|
|||||||
|
|
||||||
if let Some(val) = env_vars.get(&target_var_str) {
|
if let Some(val) = env_vars.get(&target_var_str) {
|
||||||
for suggestion in nested_suggestions(val, &nested_levels, current_span) {
|
for suggestion in nested_suggestions(val, &nested_levels, current_span) {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(suggestion);
|
||||||
options.case_sensitive,
|
|
||||||
suggestion.suggestion.value.as_bytes(),
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(suggestion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort_suggestions(&prefix_str, output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No nesting provided, return all env vars
|
// No nesting provided, return all env vars
|
||||||
for env_var in env_vars {
|
for env_var in env_vars {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
options.case_sensitive,
|
|
||||||
env_var.0.as_bytes(),
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: env_var.0,
|
value: env_var.0,
|
||||||
span: current_span,
|
span: current_span,
|
||||||
@ -91,9 +78,8 @@ impl Completer for VariableCompletion {
|
|||||||
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
|
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return sort_suggestions(&prefix_str, output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,16 +94,10 @@ impl Completer for VariableCompletion {
|
|||||||
) {
|
) {
|
||||||
for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span)
|
for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span)
|
||||||
{
|
{
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(suggestion);
|
||||||
options.case_sensitive,
|
|
||||||
suggestion.suggestion.value.as_bytes(),
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(suggestion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort_suggestions(&prefix_str, output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,28 +110,17 @@ impl Completer for VariableCompletion {
|
|||||||
if let Ok(value) = var {
|
if let Ok(value) = var {
|
||||||
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span)
|
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span)
|
||||||
{
|
{
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(suggestion);
|
||||||
options.case_sensitive,
|
|
||||||
suggestion.suggestion.value.as_bytes(),
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(suggestion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort_suggestions(&prefix_str, output, options);
|
return matcher.results();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variable completion (e.g: $en<tab> to complete $env)
|
// Variable completion (e.g: $en<tab> to complete $env)
|
||||||
for builtin in builtins {
|
for builtin in builtins {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
options.case_sensitive,
|
|
||||||
builtin.as_bytes(),
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: builtin.to_string(),
|
value: builtin.to_string(),
|
||||||
span: current_span,
|
span: current_span,
|
||||||
@ -161,7 +130,6 @@ impl Completer for VariableCompletion {
|
|||||||
kind: None,
|
kind: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: The following can be refactored (see find_commands_by_predicate() used in
|
// TODO: The following can be refactored (see find_commands_by_predicate() used in
|
||||||
// command_completions).
|
// command_completions).
|
||||||
@ -170,12 +138,7 @@ impl Completer for VariableCompletion {
|
|||||||
for scope_frame in working_set.delta.scope.iter().rev() {
|
for scope_frame in working_set.delta.scope.iter().rev() {
|
||||||
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
|
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
|
||||||
for v in &overlay_frame.vars {
|
for v in &overlay_frame.vars {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
options.case_sensitive,
|
|
||||||
v.0,
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(v.0).to_string(),
|
value: String::from_utf8_lossy(v.0).to_string(),
|
||||||
span: current_span,
|
span: current_span,
|
||||||
@ -188,7 +151,6 @@ impl Completer for VariableCompletion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Permanent state vars
|
// Permanent state vars
|
||||||
// for scope in &self.engine_state.scope {
|
// for scope in &self.engine_state.scope {
|
||||||
@ -198,12 +160,7 @@ impl Completer for VariableCompletion {
|
|||||||
.rev()
|
.rev()
|
||||||
{
|
{
|
||||||
for v in &overlay_frame.vars {
|
for v in &overlay_frame.vars {
|
||||||
if options.match_algorithm.matches_u8_insensitive(
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
options.case_sensitive,
|
|
||||||
v.0,
|
|
||||||
prefix,
|
|
||||||
) {
|
|
||||||
output.push(SemanticSuggestion {
|
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(v.0).to_string(),
|
value: String::from_utf8_lossy(v.0).to_string(),
|
||||||
span: current_span,
|
span: current_span,
|
||||||
@ -215,13 +172,8 @@ impl Completer for VariableCompletion {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
output = sort_suggestions(&prefix_str, output, options);
|
matcher.results()
|
||||||
|
|
||||||
output.dedup(); // TODO: Removes only consecutive duplicates, is it intended?
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,13 +254,3 @@ fn recursive_value(val: &Value, sublevels: &[Vec<u8>]) -> Result<Value, Span> {
|
|||||||
Ok(val.clone())
|
Ok(val.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MatchAlgorithm {
|
|
||||||
pub fn matches_u8_insensitive(&self, sensitive: bool, haystack: &[u8], needle: &[u8]) -> bool {
|
|
||||||
if sensitive {
|
|
||||||
self.matches_u8(haystack, needle)
|
|
||||||
} else {
|
|
||||||
self.matches_u8(&haystack.to_ascii_lowercase(), &needle.to_ascii_lowercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -9,6 +9,8 @@ use nu_protocol::{
|
|||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::util::print_pipeline;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct EvaluateCommandsOpts {
|
pub struct EvaluateCommandsOpts {
|
||||||
pub table_mode: Option<Value>,
|
pub table_mode: Option<Value>,
|
||||||
@ -72,7 +74,7 @@ pub fn evaluate_commands(
|
|||||||
|
|
||||||
if let Some(err) = working_set.compile_errors.first() {
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
report_compile_error(&working_set, err);
|
report_compile_error(&working_set, err);
|
||||||
// Not a fatal error, for now
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
(output, working_set.render())
|
(output, working_set.render())
|
||||||
@ -93,7 +95,7 @@ pub fn evaluate_commands(
|
|||||||
t_mode.coerce_str()?.parse().unwrap_or_default();
|
t_mode.coerce_str()?.parse().unwrap_or_default();
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline.print(engine_state, stack, no_newline, false)?;
|
print_pipeline(engine_state, stack, pipeline, no_newline)?;
|
||||||
|
|
||||||
info!("evaluate {}:{}:{}", file!(), line!(), column!());
|
info!("evaluate {}:{}:{}", file!(), line!(), column!());
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::util::eval_source;
|
use crate::util::{eval_source, print_pipeline};
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
use nu_engine::{convert_env_values, eval_block};
|
use nu_engine::{convert_env_values, eval_block};
|
||||||
use nu_parser::parse;
|
use nu_parser::parse;
|
||||||
@ -89,7 +89,7 @@ pub fn evaluate_file(
|
|||||||
|
|
||||||
if let Some(err) = working_set.compile_errors.first() {
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
report_compile_error(&working_set, err);
|
report_compile_error(&working_set, err);
|
||||||
// Not a fatal error, for now
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for blocks whose name starts with "main" and replace it with the filename.
|
// Look for blocks whose name starts with "main" and replace it with the filename.
|
||||||
@ -119,7 +119,7 @@ pub fn evaluate_file(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Print the pipeline output of the last command of the file.
|
// Print the pipeline output of the last command of the file.
|
||||||
pipeline.print(engine_state, stack, true, false)?;
|
print_pipeline(engine_state, stack, pipeline, true)?;
|
||||||
|
|
||||||
// Invoke the main command with arguments.
|
// Invoke the main command with arguments.
|
||||||
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
|
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
|
||||||
|
@ -65,8 +65,12 @@ Since this command has no output, there is no point in piping it with other comm
|
|||||||
arg.into_pipeline_data()
|
arg.into_pipeline_data()
|
||||||
.print_raw(engine_state, no_newline, to_stderr)?;
|
.print_raw(engine_state, no_newline, to_stderr)?;
|
||||||
} else {
|
} else {
|
||||||
arg.into_pipeline_data()
|
arg.into_pipeline_data().print_table(
|
||||||
.print(engine_state, stack, no_newline, to_stderr)?;
|
engine_state,
|
||||||
|
stack,
|
||||||
|
no_newline,
|
||||||
|
to_stderr,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !input.is_nothing() {
|
} else if !input.is_nothing() {
|
||||||
@ -78,7 +82,7 @@ Since this command has no output, there is no point in piping it with other comm
|
|||||||
if raw {
|
if raw {
|
||||||
input.print_raw(engine_state, no_newline, to_stderr)?;
|
input.print_raw(engine_state, no_newline, to_stderr)?;
|
||||||
} else {
|
} else {
|
||||||
input.print(engine_state, stack, no_newline, to_stderr)?;
|
input.print_table(engine_state, stack, no_newline, to_stderr)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::NushellPrompt;
|
use crate::NushellPrompt;
|
||||||
use log::trace;
|
use log::{trace, warn};
|
||||||
use nu_engine::ClosureEvalOnce;
|
use nu_engine::ClosureEvalOnce;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack},
|
engine::{EngineState, Stack},
|
||||||
@ -30,30 +30,21 @@ pub(crate) const TRANSIENT_PROMPT_MULTILINE_INDICATOR: &str =
|
|||||||
pub(crate) const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\";
|
pub(crate) const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\";
|
||||||
pub(crate) const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\";
|
pub(crate) const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\";
|
||||||
pub(crate) const PRE_EXECUTION_MARKER: &str = "\x1b]133;C\x1b\\";
|
pub(crate) const PRE_EXECUTION_MARKER: &str = "\x1b]133;C\x1b\\";
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) const POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]133;D;";
|
pub(crate) const POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]133;D;";
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) const POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
|
pub(crate) const POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
|
||||||
|
|
||||||
// OSC633 is the same as OSC133 but specifically for VSCode
|
// OSC633 is the same as OSC133 but specifically for VSCode
|
||||||
pub(crate) const VSCODE_PRE_PROMPT_MARKER: &str = "\x1b]633;A\x1b\\";
|
pub(crate) const VSCODE_PRE_PROMPT_MARKER: &str = "\x1b]633;A\x1b\\";
|
||||||
pub(crate) const VSCODE_POST_PROMPT_MARKER: &str = "\x1b]633;B\x1b\\";
|
pub(crate) const VSCODE_POST_PROMPT_MARKER: &str = "\x1b]633;B\x1b\\";
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
|
pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
|
||||||
#[allow(dead_code)]
|
|
||||||
//"\x1b]633;D;{}\x1b\\"
|
//"\x1b]633;D;{}\x1b\\"
|
||||||
pub(crate) const VSCODE_POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]633;D;";
|
pub(crate) const VSCODE_POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]633;D;";
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) const VSCODE_POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
|
pub(crate) const VSCODE_POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
|
||||||
#[allow(dead_code)]
|
|
||||||
//"\x1b]633;E;{}\x1b\\"
|
//"\x1b]633;E;{}\x1b\\"
|
||||||
pub(crate) const VSCODE_COMMANDLINE_MARKER_PREFIX: &str = "\x1b]633;E;";
|
pub(crate) const VSCODE_COMMANDLINE_MARKER_PREFIX: &str = "\x1b]633;E;";
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) const VSCODE_COMMANDLINE_MARKER_SUFFIX: &str = "\x1b\\";
|
pub(crate) const VSCODE_COMMANDLINE_MARKER_SUFFIX: &str = "\x1b\\";
|
||||||
#[allow(dead_code)]
|
|
||||||
// "\x1b]633;P;Cwd={}\x1b\\"
|
// "\x1b]633;P;Cwd={}\x1b\\"
|
||||||
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_PREFIX: &str = "\x1b]633;P;Cwd=";
|
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_PREFIX: &str = "\x1b]633;P;Cwd=";
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_SUFFIX: &str = "\x1b\\";
|
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_SUFFIX: &str = "\x1b\\";
|
||||||
|
|
||||||
pub(crate) const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
|
pub(crate) const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
|
||||||
@ -89,8 +80,13 @@ fn get_prompt_string(
|
|||||||
})
|
})
|
||||||
.and_then(|pipeline_data| {
|
.and_then(|pipeline_data| {
|
||||||
let output = pipeline_data.collect_string("", config).ok();
|
let output = pipeline_data.collect_string("", config).ok();
|
||||||
|
let ansi_output = output.map(|mut x| {
|
||||||
|
// Always reset the color at the start of the right prompt
|
||||||
|
// to ensure there is no ansi bleed over
|
||||||
|
if x.is_empty() && prompt == PROMPT_COMMAND_RIGHT {
|
||||||
|
x.insert_str(0, "\x1b[0m")
|
||||||
|
};
|
||||||
|
|
||||||
output.map(|mut x| {
|
|
||||||
// Just remove the very last newline.
|
// Just remove the very last newline.
|
||||||
if x.ends_with('\n') {
|
if x.ends_with('\n') {
|
||||||
x.pop();
|
x.pop();
|
||||||
@ -100,7 +96,11 @@ fn get_prompt_string(
|
|||||||
x.pop();
|
x.pop();
|
||||||
}
|
}
|
||||||
x
|
x
|
||||||
})
|
});
|
||||||
|
// Let's keep this for debugging purposes with nu --log-level warn
|
||||||
|
warn!("{}:{}:{} {:?}", file!(), line!(), column!(), ansi_output);
|
||||||
|
|
||||||
|
ansi_output
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -858,10 +858,10 @@ fn add_parsed_keybinding(
|
|||||||
c if c.starts_with('f') => c[1..]
|
c if c.starts_with('f') => c[1..]
|
||||||
.parse()
|
.parse()
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|num| (1..=20).contains(num))
|
.filter(|num| (1..=35).contains(num))
|
||||||
.map(KeyCode::F)
|
.map(KeyCode::F)
|
||||||
.ok_or(ShellError::InvalidValue {
|
.ok_or(ShellError::InvalidValue {
|
||||||
valid: "'f1', 'f2', ..., or 'f20'".into(),
|
valid: "'f1', 'f2', ..., or 'f35'".into(),
|
||||||
actual: format!("'{keycode}'"),
|
actual: format!("'{keycode}'"),
|
||||||
span: keybinding.keycode.span(),
|
span: keybinding.keycode.span(),
|
||||||
})?,
|
})?,
|
||||||
|
@ -16,7 +16,7 @@ use crate::{
|
|||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::SetCursorStyle;
|
||||||
use log::{error, trace, warn};
|
use log::{error, trace, warn};
|
||||||
use miette::{ErrReport, IntoDiagnostic, Result};
|
use miette::{ErrReport, IntoDiagnostic, Result};
|
||||||
use nu_cmd_base::{hook::eval_hook, util::get_editor};
|
use nu_cmd_base::util::get_editor;
|
||||||
use nu_color_config::StyleComputer;
|
use nu_color_config::StyleComputer;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use nu_engine::{convert_env_values, current_dir_str, env_to_strings};
|
use nu_engine::{convert_env_values, current_dir_str, env_to_strings};
|
||||||
@ -130,13 +130,8 @@ pub fn evaluate_repl(
|
|||||||
// escape a few things because this says so
|
// escape a few things because this says so
|
||||||
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
||||||
let cmd_text = line_editor.current_buffer_contents().to_string();
|
let cmd_text = line_editor.current_buffer_contents().to_string();
|
||||||
let len = cmd_text.len();
|
|
||||||
let mut cmd_text_chars = cmd_text[0..len].chars();
|
|
||||||
let mut replaced_cmd_text = String::with_capacity(len);
|
|
||||||
|
|
||||||
while let Some(c) = unescape_for_vscode(&mut cmd_text_chars) {
|
let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?;
|
||||||
replaced_cmd_text.push(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
run_shell_integration_osc633(
|
run_shell_integration_osc633(
|
||||||
engine_state,
|
engine_state,
|
||||||
@ -220,26 +215,41 @@ pub fn evaluate_repl(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unescape_for_vscode(text: &mut std::str::Chars) -> Option<char> {
|
fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
|
||||||
match text.next() {
|
let bytes = input
|
||||||
Some('\\') => match text.next() {
|
.chars()
|
||||||
Some('0') => Some('\x00'), // NUL '\0' (null character)
|
.flat_map(|c| {
|
||||||
Some('a') => Some('\x07'), // BEL '\a' (bell)
|
let mut buf = [0; 4]; // Buffer to hold UTF-8 bytes of the character
|
||||||
Some('b') => Some('\x08'), // BS '\b' (backspace)
|
let c_bytes = c.encode_utf8(&mut buf); // Get UTF-8 bytes for the character
|
||||||
Some('t') => Some('\x09'), // HT '\t' (horizontal tab)
|
|
||||||
Some('n') => Some('\x0a'), // LF '\n' (new line)
|
if c_bytes.len() == 1 {
|
||||||
Some('v') => Some('\x0b'), // VT '\v' (vertical tab)
|
let byte = c_bytes.as_bytes()[0];
|
||||||
Some('f') => Some('\x0c'), // FF '\f' (form feed)
|
|
||||||
Some('r') => Some('\x0d'), // CR '\r' (carriage ret)
|
match byte {
|
||||||
Some(';') => Some('\x3b'), // semi-colon
|
// Escape bytes below 0x20
|
||||||
Some('\\') => Some('\x5c'), // backslash
|
b if b < 0x20 => format!("\\x{:02X}", byte).into_bytes(),
|
||||||
Some('e') => Some('\x1b'), // escape
|
// Escape semicolon as \x3B
|
||||||
Some(c) => Some(c),
|
b';' => "\\x3B".to_string().into_bytes(),
|
||||||
None => None,
|
// Escape backslash as \\
|
||||||
},
|
b'\\' => "\\\\".to_string().into_bytes(),
|
||||||
Some(c) => Some(c),
|
// Otherwise, return the character unchanged
|
||||||
None => None,
|
_ => vec![byte],
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// pass through multi-byte characters unchanged
|
||||||
|
c_bytes.bytes().collect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
String::from_utf8(bytes).map_err(|err| ShellError::CantConvert {
|
||||||
|
to_type: "string".to_string(),
|
||||||
|
from_type: "bytes".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
help: Some(format!(
|
||||||
|
"Error {err}, Unable to convert {input} to escaped bytes"
|
||||||
|
)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
|
fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
|
||||||
@ -296,9 +306,6 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
|||||||
if let Err(err) = engine_state.merge_env(&mut stack) {
|
if let Err(err) = engine_state.merge_env(&mut stack) {
|
||||||
report_shell_error(engine_state, &err);
|
report_shell_error(engine_state, &err);
|
||||||
}
|
}
|
||||||
// Check whether $env.NU_DISABLE_IR is set, so that the user can change it in the REPL
|
|
||||||
// Temporary while IR eval is optional
|
|
||||||
stack.use_ir = !stack.has_env_var(engine_state, "NU_DISABLE_IR");
|
|
||||||
perf!("merge env", start_time, use_color);
|
perf!("merge env", start_time, use_color);
|
||||||
|
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
@ -306,20 +313,26 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
|||||||
perf!("reset signals", start_time, use_color);
|
perf!("reset signals", start_time, use_color);
|
||||||
|
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
// Right before we start our prompt and take input from the user,
|
// Right before we start our prompt and take input from the user, fire the "pre_prompt" hook
|
||||||
// fire the "pre_prompt" hook
|
if let Err(err) = hook::eval_hooks(
|
||||||
if let Some(hook) = engine_state.get_config().hooks.pre_prompt.clone() {
|
engine_state,
|
||||||
if let Err(err) = eval_hook(engine_state, &mut stack, None, vec![], &hook, "pre_prompt") {
|
&mut stack,
|
||||||
|
vec![],
|
||||||
|
&engine_state.get_config().hooks.pre_prompt.clone(),
|
||||||
|
"pre_prompt",
|
||||||
|
) {
|
||||||
report_shell_error(engine_state, &err);
|
report_shell_error(engine_state, &err);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
perf!("pre-prompt hook", start_time, use_color);
|
perf!("pre-prompt hook", start_time, use_color);
|
||||||
|
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
// Next, check all the environment variables they ask for
|
// Next, check all the environment variables they ask for
|
||||||
// fire the "env_change" hook
|
// fire the "env_change" hook
|
||||||
let env_change = engine_state.get_config().hooks.env_change.clone();
|
if let Err(error) = hook::eval_env_change_hook(
|
||||||
if let Err(error) = hook::eval_env_change_hook(env_change, engine_state, &mut stack) {
|
&engine_state.get_config().hooks.env_change.clone(),
|
||||||
|
engine_state,
|
||||||
|
&mut stack,
|
||||||
|
) {
|
||||||
report_shell_error(engine_state, &error)
|
report_shell_error(engine_state, &error)
|
||||||
}
|
}
|
||||||
perf!("env-change hook", start_time, use_color);
|
perf!("env-change hook", start_time, use_color);
|
||||||
@ -504,18 +517,17 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
|||||||
|
|
||||||
// Right before we start running the code the user gave us, fire the `pre_execution`
|
// Right before we start running the code the user gave us, fire the `pre_execution`
|
||||||
// hook
|
// hook
|
||||||
if let Some(hook) = config.hooks.pre_execution.clone() {
|
{
|
||||||
// Set the REPL buffer to the current command for the "pre_execution" hook
|
// Set the REPL buffer to the current command for the "pre_execution" hook
|
||||||
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
|
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
|
||||||
repl.buffer = repl_cmd_line_text.to_string();
|
repl.buffer = repl_cmd_line_text.to_string();
|
||||||
drop(repl);
|
drop(repl);
|
||||||
|
|
||||||
if let Err(err) = eval_hook(
|
if let Err(err) = hook::eval_hooks(
|
||||||
engine_state,
|
engine_state,
|
||||||
&mut stack,
|
&mut stack,
|
||||||
None,
|
|
||||||
vec![],
|
vec![],
|
||||||
&hook,
|
&engine_state.get_config().hooks.pre_execution.clone(),
|
||||||
"pre_execution",
|
"pre_execution",
|
||||||
) {
|
) {
|
||||||
report_shell_error(engine_state, &err);
|
report_shell_error(engine_state, &err);
|
||||||
@ -750,7 +762,7 @@ fn fill_in_result_related_history_metadata(
|
|||||||
c.duration = Some(cmd_duration);
|
c.duration = Some(cmd_duration);
|
||||||
c.exit_status = stack
|
c.exit_status = stack
|
||||||
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
||||||
.and_then(|e| e.as_i64().ok());
|
.and_then(|e| e.as_int().ok());
|
||||||
c
|
c
|
||||||
})
|
})
|
||||||
.into_diagnostic()?; // todo: don't stop repl if error here?
|
.into_diagnostic()?; // todo: don't stop repl if error here?
|
||||||
@ -1069,16 +1081,8 @@ fn run_shell_integration_osc633(
|
|||||||
|
|
||||||
// escape a few things because this says so
|
// escape a few things because this says so
|
||||||
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
||||||
|
let replaced_cmd_text =
|
||||||
let replaced_cmd_text: String = repl_cmd_line_text
|
escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
|
||||||
.chars()
|
|
||||||
.map(|c| match c {
|
|
||||||
'\n' => '\x0a',
|
|
||||||
'\r' => '\x0d',
|
|
||||||
'\x1b' => '\x1b',
|
|
||||||
_ => c,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
//OSC 633 ; E ; <commandline> [; <nonce] ST - Explicitly set the command line with an optional nonce.
|
//OSC 633 ; E ; <commandline> [; <nonce] ST - Explicitly set the command line with an optional nonce.
|
||||||
run_ansi_sequence(&format!(
|
run_ansi_sequence(&format!(
|
||||||
@ -1245,7 +1249,7 @@ fn get_command_finished_marker(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let exit_code = stack
|
let exit_code = stack
|
||||||
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
||||||
.and_then(|e| e.as_i64().ok());
|
.and_then(|e| e.as_int().ok());
|
||||||
|
|
||||||
if shell_integration_osc633 {
|
if shell_integration_osc633 {
|
||||||
if stack
|
if stack
|
||||||
@ -1356,8 +1360,7 @@ fn run_finaliziation_ansi_sequence(
|
|||||||
|
|
||||||
// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
|
// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
static DRIVE_PATH_REGEX: once_cell::sync::Lazy<fancy_regex::Regex> =
|
static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
|
||||||
once_cell::sync::Lazy::new(|| {
|
|
||||||
fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
|
fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1421,7 +1424,7 @@ fn are_session_ids_in_sync() {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_auto_cd {
|
mod test_auto_cd {
|
||||||
use super::{do_auto_cd, parse_operation, ReplOperation};
|
use super::{do_auto_cd, escape_special_vscode_bytes, parse_operation, ReplOperation};
|
||||||
use nu_path::AbsolutePath;
|
use nu_path::AbsolutePath;
|
||||||
use nu_protocol::engine::{EngineState, Stack};
|
use nu_protocol::engine::{EngineState, Stack};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
@ -1571,4 +1574,43 @@ mod test_auto_cd {
|
|||||||
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
|
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
|
||||||
check(tempdir, input, dir);
|
check(tempdir, input, dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_semicolon_test() {
|
||||||
|
let input = r#"now;is"#;
|
||||||
|
let expected = r#"now\x3Bis"#;
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_backslash_test() {
|
||||||
|
let input = r#"now\is"#;
|
||||||
|
let expected = r#"now\\is"#;
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_linefeed_test() {
|
||||||
|
let input = "now\nis";
|
||||||
|
let expected = r#"now\x0Ais"#;
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_tab_null_cr_test() {
|
||||||
|
let input = "now\t\0\ris";
|
||||||
|
let expected = r#"now\x09\x00\x0Dis"#;
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_multibyte_ok() {
|
||||||
|
let input = "now🍪is";
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(input, actual);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,8 +144,6 @@ impl Highlighter for NuHighlighter {
|
|||||||
}
|
}
|
||||||
FlatShape::Flag => add_colored_token(&shape.1, next_token),
|
FlatShape::Flag => add_colored_token(&shape.1, next_token),
|
||||||
FlatShape::Pipe => add_colored_token(&shape.1, next_token),
|
FlatShape::Pipe => add_colored_token(&shape.1, next_token),
|
||||||
FlatShape::And => add_colored_token(&shape.1, next_token),
|
|
||||||
FlatShape::Or => add_colored_token(&shape.1, next_token),
|
|
||||||
FlatShape::Redirection => add_colored_token(&shape.1, next_token),
|
FlatShape::Redirection => add_colored_token(&shape.1, next_token),
|
||||||
FlatShape::Custom(..) => add_colored_token(&shape.1, next_token),
|
FlatShape::Custom(..) => add_colored_token(&shape.1, next_token),
|
||||||
FlatShape::MatchPattern => add_colored_token(&shape.1, next_token),
|
FlatShape::MatchPattern => add_colored_token(&shape.1, next_token),
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
#![allow(clippy::byte_char_slices)]
|
||||||
|
|
||||||
use nu_cmd_base::hook::eval_hook;
|
use nu_cmd_base::hook::eval_hook;
|
||||||
use nu_engine::{eval_block, eval_block_with_early_return};
|
use nu_engine::{eval_block, eval_block_with_early_return};
|
||||||
use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents};
|
use nu_parser::{lex, parse, unescape_unquote_string, Token, TokenContents};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
cli_error::report_compile_error,
|
cli_error::report_compile_error,
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
@ -10,7 +12,7 @@ use nu_protocol::{
|
|||||||
};
|
};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use nu_utils::enable_vt_processing;
|
use nu_utils::enable_vt_processing;
|
||||||
use nu_utils::perf;
|
use nu_utils::{escape_quote_string, perf};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
// This will collect environment variables from std::env and adds them to a stack.
|
// This will collect environment variables from std::env and adds them to a stack.
|
||||||
@ -201,6 +203,35 @@ fn gather_env_vars(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print a pipeline with formatting applied based on display_output hook.
|
||||||
|
///
|
||||||
|
/// This function should be preferred when printing values resulting from a completed evaluation.
|
||||||
|
/// For values printed as part of a command's execution, such as values printed by the `print` command,
|
||||||
|
/// the `PipelineData::print_table` function should be preferred instead as it is not config-dependent.
|
||||||
|
///
|
||||||
|
/// `no_newline` controls if we need to attach newline character to output.
|
||||||
|
pub fn print_pipeline(
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
pipeline: PipelineData,
|
||||||
|
no_newline: bool,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
|
||||||
|
let pipeline = eval_hook(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
Some(pipeline),
|
||||||
|
vec![],
|
||||||
|
&hook,
|
||||||
|
"display_output",
|
||||||
|
)?;
|
||||||
|
pipeline.print_raw(engine_state, no_newline, false)
|
||||||
|
} else {
|
||||||
|
// if display_output isn't set, we should still prefer to print with some formatting
|
||||||
|
pipeline.print_table(engine_state, stack, no_newline, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn eval_source(
|
pub fn eval_source(
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
@ -267,7 +298,7 @@ fn evaluate_source(
|
|||||||
|
|
||||||
if let Some(err) = working_set.compile_errors.first() {
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
report_compile_error(&working_set, err);
|
report_compile_error(&working_set, err);
|
||||||
// Not a fatal error, for now
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
(output, working_set.render())
|
(output, working_set.render())
|
||||||
@ -281,21 +312,8 @@ fn evaluate_source(
|
|||||||
eval_block::<WithoutDebug>(engine_state, stack, &block, input)
|
eval_block::<WithoutDebug>(engine_state, stack, &block, input)
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
if let PipelineData::ByteStream(..) = pipeline {
|
let no_newline = matches!(&pipeline, &PipelineData::ByteStream(..));
|
||||||
pipeline.print(engine_state, stack, false, false)
|
print_pipeline(engine_state, stack, pipeline, no_newline)?;
|
||||||
} else if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
|
|
||||||
let pipeline = eval_hook(
|
|
||||||
engine_state,
|
|
||||||
stack,
|
|
||||||
Some(pipeline),
|
|
||||||
vec![],
|
|
||||||
&hook,
|
|
||||||
"display_output",
|
|
||||||
)?;
|
|
||||||
pipeline.print(engine_state, stack, false, false)
|
|
||||||
} else {
|
|
||||||
pipeline.print(engine_state, stack, true, false)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
296
crates/nu-cli/tests/commands/history_import.rs
Normal file
296
crates/nu-cli/tests/commands/history_import.rs
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
use nu_protocol::HistoryFileFormat;
|
||||||
|
use nu_test_support::{nu, Outcome};
|
||||||
|
use reedline::{
|
||||||
|
FileBackedHistory, History, HistoryItem, HistoryItemId, ReedlineError, SearchQuery,
|
||||||
|
SqliteBackedHistory,
|
||||||
|
};
|
||||||
|
use rstest::rstest;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
struct Test {
|
||||||
|
cfg_dir: TempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Test {
|
||||||
|
fn new(history_format: &'static str) -> Self {
|
||||||
|
let cfg_dir = tempfile::Builder::new()
|
||||||
|
.prefix("history_import_test")
|
||||||
|
.tempdir()
|
||||||
|
.unwrap();
|
||||||
|
// Assigning to $env.config.history.file_format seems to work only in startup
|
||||||
|
// configuration.
|
||||||
|
std::fs::write(
|
||||||
|
cfg_dir.path().join("env.nu"),
|
||||||
|
format!("$env.config.history.file_format = {history_format:?}"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
Self { cfg_dir }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nu(&self, cmd: impl AsRef<str>) -> Outcome {
|
||||||
|
let env = [(
|
||||||
|
"XDG_CONFIG_HOME".to_string(),
|
||||||
|
self.cfg_dir.path().to_str().unwrap().to_string(),
|
||||||
|
)];
|
||||||
|
let env_config = self.cfg_dir.path().join("env.nu");
|
||||||
|
nu!(envs: env, env_config: env_config, cmd.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_plaintext(&self) -> Result<FileBackedHistory, ReedlineError> {
|
||||||
|
FileBackedHistory::with_file(
|
||||||
|
100,
|
||||||
|
self.cfg_dir
|
||||||
|
.path()
|
||||||
|
.join("nushell")
|
||||||
|
.join(HistoryFileFormat::Plaintext.default_file_name()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_sqlite(&self) -> Result<SqliteBackedHistory, ReedlineError> {
|
||||||
|
SqliteBackedHistory::with_file(
|
||||||
|
self.cfg_dir
|
||||||
|
.path()
|
||||||
|
.join("nushell")
|
||||||
|
.join(HistoryFileFormat::Sqlite.default_file_name()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_backend(&self, format: HistoryFileFormat) -> Result<Box<dyn History>, ReedlineError> {
|
||||||
|
fn boxed(be: impl History + 'static) -> Box<dyn History> {
|
||||||
|
Box::new(be)
|
||||||
|
}
|
||||||
|
use HistoryFileFormat::*;
|
||||||
|
match format {
|
||||||
|
Plaintext => self.open_plaintext().map(boxed),
|
||||||
|
Sqlite => self.open_sqlite().map(boxed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HistorySource {
|
||||||
|
Vec(Vec<HistoryItem>),
|
||||||
|
Command(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestCase {
|
||||||
|
dst_format: HistoryFileFormat,
|
||||||
|
dst_history: Vec<HistoryItem>,
|
||||||
|
src_history: HistorySource,
|
||||||
|
want_history: Vec<HistoryItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_TEST_CASE: TestCase = TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
dst_history: Vec::new(),
|
||||||
|
src_history: HistorySource::Vec(Vec::new()),
|
||||||
|
want_history: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
impl TestCase {
|
||||||
|
fn run(self) {
|
||||||
|
use HistoryFileFormat::*;
|
||||||
|
let test = Test::new(match self.dst_format {
|
||||||
|
Plaintext => "plaintext",
|
||||||
|
Sqlite => "sqlite",
|
||||||
|
});
|
||||||
|
save_all(
|
||||||
|
&mut *test.open_backend(self.dst_format).unwrap(),
|
||||||
|
self.dst_history,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let outcome = match self.src_history {
|
||||||
|
HistorySource::Vec(src_history) => {
|
||||||
|
let src_format = match self.dst_format {
|
||||||
|
Plaintext => Sqlite,
|
||||||
|
Sqlite => Plaintext,
|
||||||
|
};
|
||||||
|
save_all(&mut *test.open_backend(src_format).unwrap(), src_history).unwrap();
|
||||||
|
test.nu("history import")
|
||||||
|
}
|
||||||
|
HistorySource::Command(cmd) => {
|
||||||
|
let mut cmd = cmd.to_string();
|
||||||
|
cmd.push_str(" | history import");
|
||||||
|
test.nu(cmd)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(outcome.status.success());
|
||||||
|
let got = query_all(&*test.open_backend(self.dst_format).unwrap()).unwrap();
|
||||||
|
|
||||||
|
// Compare just the commands first, for readability.
|
||||||
|
fn commands_only(items: &[HistoryItem]) -> Vec<&str> {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.command_line.as_str())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
assert_eq!(commands_only(&got), commands_only(&self.want_history));
|
||||||
|
// If commands match, compare full items.
|
||||||
|
assert_eq!(got, self.want_history);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_all(history: &dyn History) -> Result<Vec<HistoryItem>, ReedlineError> {
|
||||||
|
history.search(SearchQuery::everything(
|
||||||
|
reedline::SearchDirection::Forward,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_all(history: &mut dyn History, items: Vec<HistoryItem>) -> Result<(), ReedlineError> {
|
||||||
|
for item in items {
|
||||||
|
history.save(item)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_ITEM: HistoryItem = HistoryItem {
|
||||||
|
command_line: String::new(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_import_pipe_string() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
src_history: HistorySource::Command("echo bar"),
|
||||||
|
want_history: vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_import_pipe_record() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Sqlite,
|
||||||
|
src_history: HistorySource::Command("[[cwd command]; [/tmp some_command]]"),
|
||||||
|
want_history: vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "some_command".to_string(),
|
||||||
|
cwd: Some("/tmp".to_string()),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_empty_plaintext() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
src_history: HistorySource::Vec(vec![
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_empty_sqlite() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Sqlite,
|
||||||
|
src_history: HistorySource::Vec(vec![
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(2)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::plaintext(HistoryFileFormat::Plaintext)]
|
||||||
|
#[case::sqlite(HistoryFileFormat::Sqlite)]
|
||||||
|
fn to_existing(#[case] dst_format: HistoryFileFormat) {
|
||||||
|
TestCase {
|
||||||
|
dst_format,
|
||||||
|
dst_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "original-1".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "original-2".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
src_history: HistorySource::Vec(vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "new".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "original-1".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "original-2".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(2)),
|
||||||
|
command_line: "new".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
|
mod history_import;
|
||||||
mod keybindings_list;
|
mod keybindings_list;
|
||||||
mod nu_highlight;
|
mod nu_highlight;
|
||||||
|
@ -88,6 +88,27 @@ fn completer_strings_with_options() -> NuCompleter {
|
|||||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[fixture]
|
||||||
|
fn completer_strings_no_sort() -> NuCompleter {
|
||||||
|
// Create a new engine
|
||||||
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
let command = r#"
|
||||||
|
def animals [] {
|
||||||
|
{
|
||||||
|
completions: ["zzzfoo", "foo", "not matched", "abcfoo" ],
|
||||||
|
options: {
|
||||||
|
completion_algorithm: "fuzzy",
|
||||||
|
sort: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def my-command [animal: string@animals] { print $animal }"#;
|
||||||
|
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
|
// Instantiate a new completer
|
||||||
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
|
}
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
fn custom_completer() -> NuCompleter {
|
fn custom_completer() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
@ -210,6 +231,13 @@ fn customcompletions_case_insensitive(mut completer_strings_with_options: NuComp
|
|||||||
match_suggestions(&expected, &suggestions);
|
match_suggestions(&expected, &suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn customcompletions_no_sort(mut completer_strings_no_sort: NuCompleter) {
|
||||||
|
let suggestions = completer_strings_no_sort.complete("my-command foo", 14);
|
||||||
|
let expected: Vec<String> = vec!["zzzfoo".into(), "foo".into(), "abcfoo".into()];
|
||||||
|
match_suggestions(&expected, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dotnu_completions() {
|
fn dotnu_completions() {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
@ -329,6 +357,39 @@ fn file_completions() {
|
|||||||
// Match the results
|
// Match the results
|
||||||
match_suggestions(&expected_paths, &suggestions);
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
|
||||||
|
// Test completions for the current folder even with parts before the autocomplet
|
||||||
|
let target_dir = format!("cp somefile.txt {dir_str}{MAIN_SEPARATOR}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![
|
||||||
|
folder(dir.join("another")),
|
||||||
|
file(dir.join("custom_completion.nu")),
|
||||||
|
folder(dir.join("directory_completion")),
|
||||||
|
file(dir.join("nushell")),
|
||||||
|
folder(dir.join("test_a")),
|
||||||
|
folder(dir.join("test_b")),
|
||||||
|
file(dir.join(".hidden_file")),
|
||||||
|
folder(dir.join(".hidden_folder")),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let separator = '/';
|
||||||
|
let target_dir = format!("cp somefile.txt {dir_str}{separator}");
|
||||||
|
let slash_suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
let expected_slash_paths: Vec<String> = expected_paths
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.replace('\\', "/"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match_suggestions(&expected_slash_paths, &slash_suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
|
||||||
// Test completions for a file
|
// Test completions for a file
|
||||||
let target_dir = format!("cp {}", folder(dir.join("another")));
|
let target_dir = format!("cp {}", folder(dir.join("another")));
|
||||||
let suggestions = completer.complete(&target_dir, target_dir.len());
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
@ -363,6 +424,75 @@ fn file_completions() {
|
|||||||
match_suggestions(&expected_paths, &suggestions);
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_command_rest_any_args_file_completions() {
|
||||||
|
// Create a new engine
|
||||||
|
let (dir, dir_str, mut engine, mut stack) = new_engine();
|
||||||
|
let command = r#"def list [ ...args: any ] {}"#;
|
||||||
|
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
|
// Instantiate a new completer
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
// Test completions for the current folder
|
||||||
|
let target_dir = format!("list {dir_str}{MAIN_SEPARATOR}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![
|
||||||
|
folder(dir.join("another")),
|
||||||
|
file(dir.join("custom_completion.nu")),
|
||||||
|
folder(dir.join("directory_completion")),
|
||||||
|
file(dir.join("nushell")),
|
||||||
|
folder(dir.join("test_a")),
|
||||||
|
folder(dir.join("test_b")),
|
||||||
|
file(dir.join(".hidden_file")),
|
||||||
|
folder(dir.join(".hidden_folder")),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
|
||||||
|
// Test completions for the current folder even with parts before the autocomplet
|
||||||
|
let target_dir = format!("list somefile.txt {dir_str}{MAIN_SEPARATOR}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![
|
||||||
|
folder(dir.join("another")),
|
||||||
|
file(dir.join("custom_completion.nu")),
|
||||||
|
folder(dir.join("directory_completion")),
|
||||||
|
file(dir.join("nushell")),
|
||||||
|
folder(dir.join("test_a")),
|
||||||
|
folder(dir.join("test_b")),
|
||||||
|
file(dir.join(".hidden_file")),
|
||||||
|
folder(dir.join(".hidden_folder")),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
|
||||||
|
// Test completions for a file
|
||||||
|
let target_dir = format!("list {}", folder(dir.join("another")));
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![file(dir.join("another").join("newfile"))];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
|
||||||
|
// Test completions for hidden files
|
||||||
|
let target_dir = format!("list {}", file(dir.join(".hidden_folder").join(".")));
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
let expected_paths: Vec<String> =
|
||||||
|
vec![file(dir.join(".hidden_folder").join(".hidden_subfile"))];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[test]
|
#[test]
|
||||||
fn file_completions_with_mixed_separators() {
|
fn file_completions_with_mixed_separators() {
|
||||||
@ -890,8 +1020,8 @@ fn subcommand_completions(mut subcommand_completer: NuCompleter) {
|
|||||||
match_suggestions(
|
match_suggestions(
|
||||||
&vec![
|
&vec![
|
||||||
"foo bar".to_string(),
|
"foo bar".to_string(),
|
||||||
"foo aabcrr".to_string(),
|
|
||||||
"foo abaz".to_string(),
|
"foo abaz".to_string(),
|
||||||
|
"foo aabcrr".to_string(),
|
||||||
],
|
],
|
||||||
&suggestions,
|
&suggestions,
|
||||||
);
|
);
|
||||||
@ -955,8 +1085,8 @@ fn flag_completions() {
|
|||||||
"--mime-type".into(),
|
"--mime-type".into(),
|
||||||
"--short-names".into(),
|
"--short-names".into(),
|
||||||
"--threads".into(),
|
"--threads".into(),
|
||||||
"-D".into(),
|
|
||||||
"-a".into(),
|
"-a".into(),
|
||||||
|
"-D".into(),
|
||||||
"-d".into(),
|
"-d".into(),
|
||||||
"-f".into(),
|
"-f".into(),
|
||||||
"-h".into(),
|
"-h".into(),
|
||||||
@ -1287,7 +1417,7 @@ fn variables_completions() {
|
|||||||
assert_eq!(3, suggestions.len());
|
assert_eq!(3, suggestions.len());
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let expected: Vec<String> = vec!["PWD".into(), "Path".into(), "TEST".into()];
|
let expected: Vec<String> = vec!["Path".into(), "PWD".into(), "TEST".into()];
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()];
|
let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()];
|
||||||
|
|
||||||
@ -1576,6 +1706,23 @@ fn sort_fuzzy_completions_in_alphabetical_order(mut fuzzy_alpha_sort_completer:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_match() {
|
||||||
|
let (dir, _, engine, stack) = new_partial_engine();
|
||||||
|
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
let target_dir = format!("open {}", folder(dir.join("pArTiAL")));
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Since it's an exact match, only 'partial' should be suggested, not
|
||||||
|
// 'partial-a' and stuff. Implemented in #13302
|
||||||
|
match_suggestions(
|
||||||
|
&vec![file(dir.join("partial").join("hello.txt"))],
|
||||||
|
&suggestions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[ignore = "was reverted, still needs fixing"]
|
#[ignore = "was reverted, still needs fixing"]
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn alias_offset_bug_7648() {
|
fn alias_offset_bug_7648() {
|
||||||
@ -1612,13 +1759,3 @@ fn alias_offset_bug_7754() {
|
|||||||
// This crashes before PR #7756
|
// This crashes before PR #7756
|
||||||
let _suggestions = completer.complete("ll -a | c", 9);
|
let _suggestions = completer.complete("ll -a | c", 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_path_env_var_8003() {
|
|
||||||
// Create a new engine
|
|
||||||
let (_, _, engine, _) = new_engine();
|
|
||||||
// Get the path env var in a platform agnostic way
|
|
||||||
let the_path = engine.get_path_env_var();
|
|
||||||
// Make sure it's not empty
|
|
||||||
assert!(the_path.is_some());
|
|
||||||
}
|
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cmd-base"
|
name = "nu-cmd-base"
|
||||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
|
||||||
version = "0.99.1"
|
version = "0.101.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -13,10 +13,10 @@ version = "0.99.1"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
nu-engine = { path = "../nu-engine", version = "0.101.0", default-features = false }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
nu-parser = { path = "../nu-parser", version = "0.101.0" }
|
||||||
nu-path = { path = "../nu-path", version = "0.99.1" }
|
nu-path = { path = "../nu-path", version = "0.101.0" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
nu-protocol = { path = "../nu-protocol", version = "0.101.0", default-features = false }
|
||||||
|
|
||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
miette = { workspace = true }
|
miette = { workspace = true }
|
||||||
|
@ -7,46 +7,52 @@ use nu_protocol::{
|
|||||||
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
||||||
PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId,
|
PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
pub fn eval_env_change_hook(
|
pub fn eval_env_change_hook(
|
||||||
env_change_hook: Option<Value>,
|
env_change_hook: &HashMap<String, Vec<Value>>,
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
) -> Result<(), ShellError> {
|
) -> Result<(), ShellError> {
|
||||||
if let Some(hook) = env_change_hook {
|
for (env, hooks) in env_change_hook {
|
||||||
match hook {
|
let before = engine_state.previous_env_vars.get(env);
|
||||||
Value::Record { val, .. } => {
|
let after = stack.get_env_var(engine_state, env);
|
||||||
for (env_name, hook_value) in &*val {
|
|
||||||
let before = engine_state.previous_env_vars.get(env_name);
|
|
||||||
let after = stack.get_env_var(engine_state, env_name);
|
|
||||||
if before != after {
|
if before != after {
|
||||||
let before = before.cloned().unwrap_or_default();
|
let before = before.cloned().unwrap_or_default();
|
||||||
let after = after.cloned().unwrap_or_default();
|
let after = after.cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
eval_hooks(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
vec![("$before".into(), before), ("$after".into(), after.clone())],
|
||||||
|
hooks,
|
||||||
|
"env_change",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Arc::make_mut(&mut engine_state.previous_env_vars).insert(env.clone(), after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_hooks(
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
arguments: Vec<(String, Value)>,
|
||||||
|
hooks: &[Value],
|
||||||
|
hook_name: &str,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
for hook in hooks {
|
||||||
eval_hook(
|
eval_hook(
|
||||||
engine_state,
|
engine_state,
|
||||||
stack,
|
stack,
|
||||||
None,
|
None,
|
||||||
vec![("$before".into(), before), ("$after".into(), after.clone())],
|
arguments.clone(),
|
||||||
hook_value,
|
hook,
|
||||||
"env_change",
|
&format!("{hook_name} list, recursive"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Arc::make_mut(&mut engine_state.previous_env_vars)
|
|
||||||
.insert(env_name.clone(), after);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
x => {
|
|
||||||
return Err(ShellError::TypeMismatch {
|
|
||||||
err_message: "record for the 'env_change' hook".to_string(),
|
|
||||||
span: x.span(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,16 +133,7 @@ pub fn eval_hook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
for val in vals {
|
eval_hooks(engine_state, stack, arguments, vals, hook_name)?;
|
||||||
eval_hook(
|
|
||||||
engine_state,
|
|
||||||
stack,
|
|
||||||
None,
|
|
||||||
arguments.clone(),
|
|
||||||
val,
|
|
||||||
&format!("{hook_name} list, recursive"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Value::Record { val, .. } => {
|
Value::Record { val, .. } => {
|
||||||
// Hooks can optionally be a record in this form:
|
// Hooks can optionally be a record in this form:
|
||||||
|
@ -78,10 +78,10 @@ pub fn get_editor(
|
|||||||
get_editor_commandline(&config.buffer_editor, "$env.config.buffer_editor")
|
get_editor_commandline(&config.buffer_editor, "$env.config.buffer_editor")
|
||||||
{
|
{
|
||||||
Ok(buff_editor)
|
Ok(buff_editor)
|
||||||
} else if let Some(value) = env_vars.get("EDITOR") {
|
|
||||||
get_editor_commandline(value, "$env.EDITOR")
|
|
||||||
} else if let Some(value) = env_vars.get("VISUAL") {
|
} else if let Some(value) = env_vars.get("VISUAL") {
|
||||||
get_editor_commandline(value, "$env.VISUAL")
|
get_editor_commandline(value, "$env.VISUAL")
|
||||||
|
} else if let Some(value) = env_vars.get("EDITOR") {
|
||||||
|
get_editor_commandline(value, "$env.EDITOR")
|
||||||
} else {
|
} else {
|
||||||
Err(ShellError::GenericError {
|
Err(ShellError::GenericError {
|
||||||
error: "No editor configured".into(),
|
error: "No editor configured".into(),
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cmd-extra"
|
name = "nu-cmd-extra"
|
||||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
|
||||||
version = "0.99.1"
|
version = "0.101.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -16,13 +16,13 @@ bench = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.99.1" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.101.0" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
nu-engine = { path = "../nu-engine", version = "0.101.0", default-features = false }
|
||||||
nu-json = { version = "0.99.1", path = "../nu-json" }
|
nu-json = { version = "0.101.0", path = "../nu-json" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
nu-parser = { path = "../nu-parser", version = "0.101.0" }
|
||||||
nu-pretty-hex = { version = "0.99.1", path = "../nu-pretty-hex" }
|
nu-pretty-hex = { version = "0.101.0", path = "../nu-pretty-hex" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
nu-protocol = { path = "../nu-protocol", version = "0.101.0", default-features = false }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.99.1" }
|
nu-utils = { path = "../nu-utils", version = "0.101.0", default-features = false }
|
||||||
|
|
||||||
# Potential dependencies for extras
|
# Potential dependencies for extras
|
||||||
heck = { workspace = true }
|
heck = { workspace = true }
|
||||||
@ -36,6 +36,6 @@ v_htmlescape = { workspace = true }
|
|||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.99.1" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.101.0" }
|
||||||
nu-command = { path = "../nu-command", version = "0.99.1" }
|
nu-command = { path = "../nu-command", version = "0.101.0" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.99.1" }
|
nu-test-support = { path = "../nu-test-support", version = "0.101.0" }
|
@ -203,7 +203,7 @@ pub fn action(input: &Value, _args: &Arguments, span: Span) -> Value {
|
|||||||
Value::string(raw_string.trim(), span)
|
Value::string(raw_string.trim(), span)
|
||||||
}
|
}
|
||||||
Value::Int { val, .. } => convert_to_smallest_number_type(*val, span),
|
Value::Int { val, .. } => convert_to_smallest_number_type(*val, span),
|
||||||
Value::Filesize { val, .. } => convert_to_smallest_number_type(*val, span),
|
Value::Filesize { val, .. } => convert_to_smallest_number_type(val.get(), span),
|
||||||
Value::Duration { val, .. } => convert_to_smallest_number_type(*val, span),
|
Value::Duration { val, .. } => convert_to_smallest_number_type(*val, span),
|
||||||
Value::String { val, .. } => {
|
Value::String { val, .. } => {
|
||||||
let raw_bytes = val.as_bytes();
|
let raw_bytes = val.as_bytes();
|
||||||
|
@ -66,7 +66,7 @@ fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
|
|||||||
match input {
|
match input {
|
||||||
Value::Float { val, .. } => fmt_it_64(*val, span),
|
Value::Float { val, .. } => fmt_it_64(*val, span),
|
||||||
Value::Int { val, .. } => fmt_it(*val, span),
|
Value::Int { val, .. } => fmt_it(*val, span),
|
||||||
Value::Filesize { val, .. } => fmt_it(*val, span),
|
Value::Filesize { val, .. } => fmt_it(val.get(), span),
|
||||||
// Propagate errors by explicitly matching them before the final case.
|
// Propagate errors by explicitly matching them before the final case.
|
||||||
Value::Error { .. } => input.clone(),
|
Value::Error { .. } => input.clone(),
|
||||||
other => Value::error(
|
other => Value::error(
|
||||||
|
@ -25,7 +25,7 @@ impl Command for EachWhile {
|
|||||||
)])
|
)])
|
||||||
.required(
|
.required(
|
||||||
"closure",
|
"closure",
|
||||||
SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])),
|
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
|
||||||
"the closure to run",
|
"the closure to run",
|
||||||
)
|
)
|
||||||
.category(Category::Filters)
|
.category(Category::Filters)
|
||||||
|
@ -2,4 +2,4 @@ mod from;
|
|||||||
mod to;
|
mod to;
|
||||||
|
|
||||||
pub(crate) use from::url::FromUrl;
|
pub(crate) use from::url::FromUrl;
|
||||||
pub(crate) use to::html::ToHtml;
|
pub use to::html::ToHtml;
|
||||||
|
@ -9,6 +9,7 @@ mod strings;
|
|||||||
pub use bits::{
|
pub use bits::{
|
||||||
Bits, BitsAnd, BitsInto, BitsNot, BitsOr, BitsRol, BitsRor, BitsShl, BitsShr, BitsXor,
|
Bits, BitsAnd, BitsInto, BitsNot, BitsOr, BitsRol, BitsRor, BitsShl, BitsShr, BitsXor,
|
||||||
};
|
};
|
||||||
|
pub use formats::ToHtml;
|
||||||
pub use math::{MathArcCos, MathArcCosH, MathArcSin, MathArcSinH, MathArcTan, MathArcTanH};
|
pub use math::{MathArcCos, MathArcCosH, MathArcSin, MathArcSinH, MathArcTan, MathArcTanH};
|
||||||
pub use math::{MathCos, MathCosH, MathSin, MathSinH, MathTan, MathTanH};
|
pub use math::{MathCos, MathCosH, MathSin, MathSinH, MathTan, MathTanH};
|
||||||
pub use math::{MathExp, MathLn};
|
pub use math::{MathExp, MathLn};
|
||||||
@ -54,7 +55,8 @@ pub fn add_extra_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
strings::str_::case::StrTitleCase
|
strings::str_::case::StrTitleCase
|
||||||
);
|
);
|
||||||
|
|
||||||
bind_command!(formats::ToHtml, formats::FromUrl);
|
bind_command!(ToHtml, formats::FromUrl);
|
||||||
|
|
||||||
// Bits
|
// Bits
|
||||||
bind_command! {
|
bind_command! {
|
||||||
Bits,
|
Bits,
|
||||||
|
@ -6,7 +6,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cmd-lang"
|
name = "nu-cmd-lang"
|
||||||
version = "0.99.1"
|
version = "0.101.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
bench = false
|
bench = false
|
||||||
@ -15,18 +15,29 @@ bench = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
nu-engine = { path = "../nu-engine", version = "0.101.0", default-features = false }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
nu-parser = { path = "../nu-parser", version = "0.101.0" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
nu-protocol = { path = "../nu-protocol", version = "0.101.0", default-features = false }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.99.1" }
|
nu-utils = { path = "../nu-utils", version = "0.101.0", default-features = false }
|
||||||
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
shadow-rs = { version = "0.35", default-features = false }
|
shadow-rs = { version = "0.37", default-features = false }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
shadow-rs = { version = "0.35", default-features = false }
|
shadow-rs = { version = "0.37", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["os"]
|
||||||
|
os = [
|
||||||
|
"nu-engine/os",
|
||||||
|
"nu-protocol/os",
|
||||||
|
"nu-utils/os",
|
||||||
|
]
|
||||||
|
plugin = [
|
||||||
|
"nu-protocol/plugin",
|
||||||
|
"os",
|
||||||
|
]
|
||||||
|
|
||||||
mimalloc = []
|
mimalloc = []
|
||||||
trash-support = []
|
trash-support = []
|
||||||
sqlite = []
|
sqlite = []
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn main() -> shadow_rs::SdResult<()> {
|
fn main() {
|
||||||
// Look up the current Git commit ourselves instead of relying on shadow_rs,
|
// Look up the current Git commit ourselves instead of relying on shadow_rs,
|
||||||
// because shadow_rs does it in a really slow-to-compile way (it builds libgit2)
|
// because shadow_rs does it in a really slow-to-compile way (it builds libgit2)
|
||||||
let hash = get_git_hash().unwrap_or_default();
|
let hash = get_git_hash().unwrap_or_default();
|
||||||
println!("cargo:rustc-env=NU_COMMIT_HASH={hash}");
|
println!("cargo:rustc-env=NU_COMMIT_HASH={hash}");
|
||||||
|
shadow_rs::ShadowBuilder::builder()
|
||||||
shadow_rs::new()
|
.build()
|
||||||
|
.expect("shadow builder build should success");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_git_hash() -> Option<String> {
|
fn get_git_hash() -> Option<String> {
|
||||||
|
@ -169,6 +169,7 @@ fn run(
|
|||||||
let origin = match stream.source() {
|
let origin = match stream.source() {
|
||||||
ByteStreamSource::Read(_) => "unknown",
|
ByteStreamSource::Read(_) => "unknown",
|
||||||
ByteStreamSource::File(_) => "file",
|
ByteStreamSource::File(_) => "file",
|
||||||
|
#[cfg(feature = "os")]
|
||||||
ByteStreamSource::Child(_) => "external",
|
ByteStreamSource::Child(_) => "external",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
use nu_engine::{command_prelude::*, get_eval_block_with_early_return, redirect_env};
|
use nu_engine::{command_prelude::*, get_eval_block_with_early_return, redirect_env};
|
||||||
use nu_protocol::{
|
#[cfg(feature = "os")]
|
||||||
engine::Closure,
|
use nu_protocol::process::{ChildPipe, ChildProcess};
|
||||||
process::{ChildPipe, ChildProcess},
|
use nu_protocol::{engine::Closure, ByteStream, ByteStreamSource, OutDest};
|
||||||
ByteStream, ByteStreamSource, OutDest,
|
|
||||||
};
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{Cursor, Read},
|
io::{Cursor, Read},
|
||||||
thread,
|
thread,
|
||||||
@ -69,6 +68,33 @@ impl Command for Do {
|
|||||||
let block: Closure = call.req(engine_state, caller_stack, 0)?;
|
let block: Closure = call.req(engine_state, caller_stack, 0)?;
|
||||||
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
|
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
|
||||||
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?;
|
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?;
|
||||||
|
|
||||||
|
if call.has_flag(engine_state, caller_stack, "ignore-shell-errors")? {
|
||||||
|
nu_protocol::report_shell_warning(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Deprecated option".into(),
|
||||||
|
msg: "`--ignore-shell-errors` is deprecated and will be removed in 0.102.0."
|
||||||
|
.into(),
|
||||||
|
span: Some(call.head),
|
||||||
|
help: Some("Please use the `--ignore-errors(-i)`".into()),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if call.has_flag(engine_state, caller_stack, "ignore-program-errors")? {
|
||||||
|
nu_protocol::report_shell_warning(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Deprecated option".into(),
|
||||||
|
msg: "`--ignore-program-errors` is deprecated and will be removed in 0.102.0."
|
||||||
|
.into(),
|
||||||
|
span: Some(call.head),
|
||||||
|
help: Some("Please use the `--ignore-errors(-i)`".into()),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
let ignore_shell_errors = ignore_all_errors
|
let ignore_shell_errors = ignore_all_errors
|
||||||
|| call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?;
|
|| call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?;
|
||||||
let ignore_program_errors = ignore_all_errors
|
let ignore_program_errors = ignore_all_errors
|
||||||
@ -82,9 +108,6 @@ impl Command for Do {
|
|||||||
bind_args_to(&mut callee_stack, &block.signature, rest, head)?;
|
bind_args_to(&mut callee_stack, &block.signature, rest, head)?;
|
||||||
let eval_block_with_early_return = get_eval_block_with_early_return(engine_state);
|
let eval_block_with_early_return = get_eval_block_with_early_return(engine_state);
|
||||||
|
|
||||||
// Applies to all block evaluation once set true
|
|
||||||
callee_stack.use_ir = !caller_stack.has_env_var(engine_state, "NU_DISABLE_IR");
|
|
||||||
|
|
||||||
let result = eval_block_with_early_return(engine_state, &mut callee_stack, block, input);
|
let result = eval_block_with_early_return(engine_state, &mut callee_stack, block, input);
|
||||||
|
|
||||||
if has_env {
|
if has_env {
|
||||||
@ -95,6 +118,13 @@ impl Command for Do {
|
|||||||
match result {
|
match result {
|
||||||
Ok(PipelineData::ByteStream(stream, metadata)) if capture_errors => {
|
Ok(PipelineData::ByteStream(stream, metadata)) if capture_errors => {
|
||||||
let span = stream.span();
|
let span = stream.span();
|
||||||
|
#[cfg(not(feature = "os"))]
|
||||||
|
return Err(ShellError::DisabledOsSupport {
|
||||||
|
msg: "Cannot create a thread to receive stdout message.".to_string(),
|
||||||
|
span: Some(span),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "os")]
|
||||||
match stream.into_child() {
|
match stream.into_child() {
|
||||||
Ok(mut child) => {
|
Ok(mut child) => {
|
||||||
// Use a thread to receive stdout message.
|
// Use a thread to receive stdout message.
|
||||||
@ -172,6 +202,7 @@ impl Command for Do {
|
|||||||
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value
|
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
|
#[cfg(feature = "os")]
|
||||||
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
||||||
child.ignore_error(true);
|
child.ignore_error(true);
|
||||||
}
|
}
|
||||||
@ -211,16 +242,6 @@ impl Command for Do {
|
|||||||
example: r#"do --ignore-errors { thisisnotarealcommand }"#,
|
example: r#"do --ignore-errors { thisisnotarealcommand }"#,
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
|
||||||
description: "Run the closure and ignore shell errors",
|
|
||||||
example: r#"do --ignore-shell-errors { thisisnotarealcommand }"#,
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
Example {
|
|
||||||
description: "Run the closure and ignore external program errors",
|
|
||||||
example: r#"do --ignore-program-errors { nu --commands 'exit 1' }; echo "I'll still run""#,
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
Example {
|
Example {
|
||||||
description: "Abort the pipeline if a program returns a non-zero exit code",
|
description: "Abort the pipeline if a program returns a non-zero exit code",
|
||||||
example: r#"do --capture-errors { nu --commands 'exit 1' } | myscarycommand"#,
|
example: r#"do --capture-errors { nu --commands 'exit 1' } | myscarycommand"#,
|
||||||
|
@ -35,6 +35,7 @@ impl Command for Ignore {
|
|||||||
mut input: PipelineData,
|
mut input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
if let PipelineData::ByteStream(stream, _) = &mut input {
|
if let PipelineData::ByteStream(stream, _) = &mut input {
|
||||||
|
#[cfg(feature = "os")]
|
||||||
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
||||||
child.ignore_error(true);
|
child.ignore_error(true);
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ fn run_catch(
|
|||||||
|
|
||||||
if let Some(catch) = catch {
|
if let Some(catch) = catch {
|
||||||
stack.set_last_error(&error);
|
stack.set_last_error(&error);
|
||||||
let error = error.into_value(span);
|
let error = error.into_value(&StateWorkingSet::new(engine_state), span);
|
||||||
let block = engine_state.get_block(catch.block_id);
|
let block = engine_state.get_block(catch.block_id);
|
||||||
// Put the error value in the positional closure var
|
// Put the error value in the positional closure var
|
||||||
if let Some(var) = block.signature.get_positional(0) {
|
if let Some(var) = block.signature.get_positional(0) {
|
||||||
|
@ -98,15 +98,21 @@ This command is a parser keyword. For details, check:
|
|||||||
engine_state.get_span_contents(import_pattern.head.span),
|
engine_state.get_span_contents(import_pattern.head.span),
|
||||||
);
|
);
|
||||||
|
|
||||||
let maybe_file_path = find_in_dirs_env(
|
let maybe_file_path_or_dir = find_in_dirs_env(
|
||||||
&module_arg_str,
|
&module_arg_str,
|
||||||
engine_state,
|
engine_state,
|
||||||
caller_stack,
|
caller_stack,
|
||||||
get_dirs_var_from_call(caller_stack, call),
|
get_dirs_var_from_call(caller_stack, call),
|
||||||
)?;
|
)?;
|
||||||
let maybe_parent = maybe_file_path
|
// module_arg_str maybe a directory, in this case
|
||||||
.as_ref()
|
// find_in_dirs_env returns a directory.
|
||||||
.and_then(|path| path.parent().map(|p| p.to_path_buf()));
|
let maybe_parent = maybe_file_path_or_dir.as_ref().and_then(|path| {
|
||||||
|
if path.is_dir() {
|
||||||
|
Some(path.to_path_buf())
|
||||||
|
} else {
|
||||||
|
path.parent().map(|p| p.to_path_buf())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut callee_stack = caller_stack
|
let mut callee_stack = caller_stack
|
||||||
.gather_captures(engine_state, &block.captures)
|
.gather_captures(engine_state, &block.captures)
|
||||||
@ -118,9 +124,15 @@ This command is a parser keyword. For details, check:
|
|||||||
callee_stack.add_env_var("FILE_PWD".to_string(), file_pwd);
|
callee_stack.add_env_var("FILE_PWD".to_string(), file_pwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(file_path) = maybe_file_path {
|
if let Some(path) = maybe_file_path_or_dir {
|
||||||
let file_path = Value::string(file_path.to_string_lossy(), call.head);
|
let module_file_path = if path.is_dir() {
|
||||||
callee_stack.add_env_var("CURRENT_FILE".to_string(), file_path);
|
// the existence of `mod.nu` is verified in parsing time
|
||||||
|
// so it's safe to use it here.
|
||||||
|
Value::string(path.join("mod.nu").to_string_lossy(), call.head)
|
||||||
|
} else {
|
||||||
|
Value::string(path.to_string_lossy(), call.head)
|
||||||
|
};
|
||||||
|
callee_stack.add_env_var("CURRENT_FILE".to_string(), module_file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let eval_block = get_eval_block(engine_state);
|
let eval_block = get_eval_block(engine_state);
|
||||||
|
@ -116,6 +116,11 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
|
|||||||
Value::string(features_enabled().join(", "), span),
|
Value::string(features_enabled().join(", "), span),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "plugin"))]
|
||||||
|
let _ = engine_state;
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
{
|
||||||
// Get a list of plugin names and versions if present
|
// Get a list of plugin names and versions if present
|
||||||
let installed_plugins = engine_state
|
let installed_plugins = engine_state
|
||||||
.plugins()
|
.plugins()
|
||||||
@ -134,6 +139,7 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
|
|||||||
"installed_plugins",
|
"installed_plugins",
|
||||||
Value::string(installed_plugins.join(", "), span),
|
Value::string(installed_plugins.join(", "), span),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Value::record(record, span).into_pipeline_data())
|
Ok(Value::record(record, span).into_pipeline_data())
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#![cfg_attr(not(feature = "os"), allow(unused))]
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
mod core_commands;
|
mod core_commands;
|
||||||
mod default_context;
|
mod default_context;
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cmd-plugin"
|
name = "nu-cmd-plugin"
|
||||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
|
||||||
version = "0.99.1"
|
version = "0.101.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -13,10 +13,10 @@ version = "0.99.1"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
nu-engine = { path = "../nu-engine", version = "0.101.0" }
|
||||||
nu-path = { path = "../nu-path", version = "0.99.1" }
|
nu-path = { path = "../nu-path", version = "0.101.0" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1", features = ["plugin"] }
|
nu-protocol = { path = "../nu-protocol", version = "0.101.0", features = ["plugin"] }
|
||||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.99.1" }
|
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.101.0" }
|
||||||
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ apparent the next time `nu` is next launched with that plugin registry file.
|
|||||||
let metadata = interface.get_metadata()?;
|
let metadata = interface.get_metadata()?;
|
||||||
let commands = interface.get_signature()?;
|
let commands = interface.get_signature()?;
|
||||||
|
|
||||||
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
modify_plugin_file(engine_state, stack, call.head, &custom_path, |contents| {
|
||||||
// Update the file with the received metadata and signatures
|
// Update the file with the received metadata and signatures
|
||||||
let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
|
let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
|
||||||
contents.upsert_plugin(item);
|
contents.upsert_plugin(item);
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use itertools::Itertools;
|
use itertools::{EitherOrBoth, Itertools};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{IntoValue, PluginRegistryItemData};
|
||||||
|
|
||||||
|
use crate::util::read_plugin_file;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PluginList;
|
pub struct PluginList;
|
||||||
@ -17,7 +20,7 @@ impl Command for PluginList {
|
|||||||
[
|
[
|
||||||
("name".into(), Type::String),
|
("name".into(), Type::String),
|
||||||
("version".into(), Type::String),
|
("version".into(), Type::String),
|
||||||
("is_running".into(), Type::Bool),
|
("status".into(), Type::String),
|
||||||
("pid".into(), Type::Int),
|
("pid".into(), Type::Int),
|
||||||
("filename".into(), Type::String),
|
("filename".into(), Type::String),
|
||||||
("shell".into(), Type::String),
|
("shell".into(), Type::String),
|
||||||
@ -26,11 +29,54 @@ impl Command for PluginList {
|
|||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.named(
|
||||||
|
"plugin-config",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"Use a plugin registry file other than the one set in `$nu.plugin-path`",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"engine",
|
||||||
|
"Show info for plugins that are loaded into the engine only.",
|
||||||
|
Some('e'),
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"registry",
|
||||||
|
"Show info for plugins from the registry file only.",
|
||||||
|
Some('r'),
|
||||||
|
)
|
||||||
.category(Category::Plugin)
|
.category(Category::Plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"List installed plugins."
|
"List loaded and installed plugins."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"
|
||||||
|
The `status` column will contain one of the following values:
|
||||||
|
|
||||||
|
- `added`: The plugin is present in the plugin registry file, but not in
|
||||||
|
the engine.
|
||||||
|
- `loaded`: The plugin is present both in the plugin registry file and in
|
||||||
|
the engine, but is not running.
|
||||||
|
- `running`: The plugin is currently running, and the `pid` column should
|
||||||
|
contain its process ID.
|
||||||
|
- `modified`: The plugin state present in the plugin registry file is different
|
||||||
|
from the state in the engine.
|
||||||
|
- `removed`: The plugin is still loaded in the engine, but is not present in
|
||||||
|
the plugin registry file.
|
||||||
|
- `invalid`: The data in the plugin registry file couldn't be deserialized,
|
||||||
|
and the plugin most likely needs to be added again.
|
||||||
|
|
||||||
|
`running` takes priority over any other status. Unless `--registry` is used
|
||||||
|
or the plugin has not been loaded yet, the values of `version`, `filename`,
|
||||||
|
`shell`, and `commands` reflect the values in the engine and not the ones in
|
||||||
|
the plugin registry file.
|
||||||
|
|
||||||
|
See also: `plugin use`
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_terms(&self) -> Vec<&str> {
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
@ -45,7 +91,7 @@ impl Command for PluginList {
|
|||||||
result: Some(Value::test_list(vec![Value::test_record(record! {
|
result: Some(Value::test_list(vec![Value::test_record(record! {
|
||||||
"name" => Value::test_string("inc"),
|
"name" => Value::test_string("inc"),
|
||||||
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
|
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
|
||||||
"is_running" => Value::test_bool(true),
|
"status" => Value::test_string("running"),
|
||||||
"pid" => Value::test_int(106480),
|
"pid" => Value::test_int(106480),
|
||||||
"filename" => if cfg!(windows) {
|
"filename" => if cfg!(windows) {
|
||||||
Value::test_string(r"C:\nu\plugins\nu_plugin_inc.exe")
|
Value::test_string(r"C:\nu\plugins\nu_plugin_inc.exe")
|
||||||
@ -67,12 +113,58 @@ impl Command for PluginList {
|
|||||||
fn run(
|
fn run(
|
||||||
&self,
|
&self,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
_stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
|
||||||
|
let engine_mode = call.has_flag(engine_state, stack, "engine")?;
|
||||||
|
let registry_mode = call.has_flag(engine_state, stack, "registry")?;
|
||||||
|
|
||||||
|
let plugins_info = match (engine_mode, registry_mode) {
|
||||||
|
// --engine and --registry together is equivalent to the default.
|
||||||
|
(false, false) | (true, true) => {
|
||||||
|
if engine_state.plugin_path.is_some() || custom_path.is_some() {
|
||||||
|
let plugins_in_engine = get_plugins_in_engine(engine_state);
|
||||||
|
let plugins_in_registry =
|
||||||
|
get_plugins_in_registry(engine_state, stack, call.head, &custom_path)?;
|
||||||
|
merge_plugin_info(plugins_in_engine, plugins_in_registry)
|
||||||
|
} else {
|
||||||
|
// Don't produce error when running nu --no-config-file
|
||||||
|
get_plugins_in_engine(engine_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(true, false) => get_plugins_in_engine(engine_state),
|
||||||
|
(false, true) => get_plugins_in_registry(engine_state, stack, call.head, &custom_path)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(plugins_info.into_value(call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, IntoValue, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
struct PluginInfo {
|
||||||
|
name: String,
|
||||||
|
version: Option<String>,
|
||||||
|
status: PluginStatus,
|
||||||
|
pid: Option<u32>,
|
||||||
|
filename: String,
|
||||||
|
shell: Option<String>,
|
||||||
|
commands: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, IntoValue, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
#[nu_value(rename_all = "snake_case")]
|
||||||
|
enum PluginStatus {
|
||||||
|
Added,
|
||||||
|
Loaded,
|
||||||
|
Running,
|
||||||
|
Modified,
|
||||||
|
Removed,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_plugins_in_engine(engine_state: &EngineState) -> Vec<PluginInfo> {
|
||||||
// Group plugin decls by plugin identity
|
// Group plugin decls by plugin identity
|
||||||
let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
|
let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
|
||||||
decl.plugin_identity()
|
decl.plugin_identity()
|
||||||
@ -80,45 +172,130 @@ impl Command for PluginList {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build plugins list
|
// Build plugins list
|
||||||
let list = engine_state.plugins().iter().map(|plugin| {
|
engine_state
|
||||||
|
.plugins()
|
||||||
|
.iter()
|
||||||
|
.map(|plugin| {
|
||||||
// Find commands that belong to the plugin
|
// Find commands that belong to the plugin
|
||||||
let commands = decls.get(plugin.identity())
|
let commands = decls
|
||||||
|
.get(plugin.identity())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|decls| {
|
.flat_map(|decls| decls.iter().map(|decl| decl.name().to_owned()))
|
||||||
decls.iter().map(|decl| Value::string(decl.name(), head))
|
.sorted()
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let pid = plugin
|
PluginInfo {
|
||||||
.pid()
|
name: plugin.identity().name().into(),
|
||||||
.map(|p| Value::int(p as i64, head))
|
version: plugin.metadata().and_then(|m| m.version),
|
||||||
.unwrap_or(Value::nothing(head));
|
status: if plugin.pid().is_some() {
|
||||||
|
PluginStatus::Running
|
||||||
let shell = plugin
|
} else {
|
||||||
|
PluginStatus::Loaded
|
||||||
|
},
|
||||||
|
pid: plugin.pid(),
|
||||||
|
filename: plugin.identity().filename().to_string_lossy().into_owned(),
|
||||||
|
shell: plugin
|
||||||
.identity()
|
.identity()
|
||||||
.shell()
|
.shell()
|
||||||
.map(|s| Value::string(s.to_string_lossy(), head))
|
.map(|path| path.to_string_lossy().into_owned()),
|
||||||
.unwrap_or(Value::nothing(head));
|
commands,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sorted()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
let metadata = plugin.metadata();
|
fn get_plugins_in_registry(
|
||||||
let version = metadata
|
engine_state: &EngineState,
|
||||||
.and_then(|m| m.version)
|
stack: &mut Stack,
|
||||||
.map(|s| Value::string(s, head))
|
span: Span,
|
||||||
.unwrap_or(Value::nothing(head));
|
custom_path: &Option<Spanned<String>>,
|
||||||
|
) -> Result<Vec<PluginInfo>, ShellError> {
|
||||||
|
let plugin_file_contents = read_plugin_file(engine_state, stack, span, custom_path)?;
|
||||||
|
|
||||||
let record = record! {
|
let plugins_info = plugin_file_contents
|
||||||
"name" => Value::string(plugin.identity().name(), head),
|
.plugins
|
||||||
"version" => version,
|
.into_iter()
|
||||||
"is_running" => Value::bool(plugin.is_running(), head),
|
.map(|plugin| {
|
||||||
"pid" => pid,
|
let mut info = PluginInfo {
|
||||||
"filename" => Value::string(plugin.identity().filename().to_string_lossy(), head),
|
name: plugin.name,
|
||||||
"shell" => shell,
|
version: None,
|
||||||
"commands" => Value::list(commands, head),
|
status: PluginStatus::Added,
|
||||||
|
pid: None,
|
||||||
|
filename: plugin.filename.to_string_lossy().into_owned(),
|
||||||
|
shell: plugin.shell.map(|path| path.to_string_lossy().into_owned()),
|
||||||
|
commands: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
Value::record(record, head)
|
if let PluginRegistryItemData::Valid { metadata, commands } = plugin.data {
|
||||||
}).collect();
|
info.version = metadata.version;
|
||||||
|
info.commands = commands
|
||||||
|
.into_iter()
|
||||||
|
.map(|command| command.sig.name)
|
||||||
|
.sorted()
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
info.status = PluginStatus::Invalid;
|
||||||
|
}
|
||||||
|
info
|
||||||
|
})
|
||||||
|
.sorted()
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Value::list(list, head).into_pipeline_data())
|
Ok(plugins_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If no options are provided, the command loads from both the plugin list in the engine and what's
|
||||||
|
/// in the registry file. We need to reconcile the two to set the proper states and make sure that
|
||||||
|
/// new plugins that were added to the plugin registry file show up.
|
||||||
|
fn merge_plugin_info(
|
||||||
|
from_engine: Vec<PluginInfo>,
|
||||||
|
from_registry: Vec<PluginInfo>,
|
||||||
|
) -> Vec<PluginInfo> {
|
||||||
|
from_engine
|
||||||
|
.into_iter()
|
||||||
|
.merge_join_by(from_registry, |info_a, info_b| {
|
||||||
|
info_a.name.cmp(&info_b.name)
|
||||||
|
})
|
||||||
|
.map(|either_or_both| match either_or_both {
|
||||||
|
// Exists in the engine, but not in the registry file
|
||||||
|
EitherOrBoth::Left(info) => PluginInfo {
|
||||||
|
status: match info.status {
|
||||||
|
PluginStatus::Running => info.status,
|
||||||
|
// The plugin is not in the registry file, so it should be marked as `removed`
|
||||||
|
_ => PluginStatus::Removed,
|
||||||
|
},
|
||||||
|
..info
|
||||||
|
},
|
||||||
|
// Exists in the registry file, but not in the engine
|
||||||
|
EitherOrBoth::Right(info) => info,
|
||||||
|
// Exists in both
|
||||||
|
EitherOrBoth::Both(info_engine, info_registry) => PluginInfo {
|
||||||
|
status: match (info_engine.status, info_registry.status) {
|
||||||
|
// Above all, `running` should be displayed if the plugin is running
|
||||||
|
(PluginStatus::Running, _) => PluginStatus::Running,
|
||||||
|
// `invalid` takes precedence over other states because the user probably wants
|
||||||
|
// to fix it
|
||||||
|
(_, PluginStatus::Invalid) => PluginStatus::Invalid,
|
||||||
|
// Display `modified` if the state in the registry is different somehow
|
||||||
|
_ if info_engine.is_modified(&info_registry) => PluginStatus::Modified,
|
||||||
|
// Otherwise, `loaded` (it's not running)
|
||||||
|
_ => PluginStatus::Loaded,
|
||||||
|
},
|
||||||
|
..info_engine
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.sorted()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginInfo {
|
||||||
|
/// True if the plugin info shows some kind of change (other than status/pid) relative to the
|
||||||
|
/// other
|
||||||
|
fn is_modified(&self, other: &PluginInfo) -> bool {
|
||||||
|
self.name != other.name
|
||||||
|
|| self.filename != other.filename
|
||||||
|
|| self.shell != other.shell
|
||||||
|
|| self.commands != other.commands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ fixed with `plugin add`.
|
|||||||
|
|
||||||
let filename = canonicalize_possible_filename_arg(engine_state, stack, &name.item);
|
let filename = canonicalize_possible_filename_arg(engine_state, stack, &name.item);
|
||||||
|
|
||||||
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
modify_plugin_file(engine_state, stack, call.head, &custom_path, |contents| {
|
||||||
if let Some(index) = contents
|
if let Some(index) = contents
|
||||||
.plugins
|
.plugins
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -6,18 +6,17 @@ use std::{
|
|||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn modify_plugin_file(
|
fn get_plugin_registry_file_path(
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
span: Span,
|
span: Span,
|
||||||
custom_path: Option<Spanned<String>>,
|
custom_path: &Option<Spanned<String>>,
|
||||||
operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>,
|
) -> Result<PathBuf, ShellError> {
|
||||||
) -> Result<(), ShellError> {
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
|
|
||||||
let plugin_registry_file_path = if let Some(ref custom_path) = custom_path {
|
if let Some(ref custom_path) = custom_path {
|
||||||
nu_path::expand_path_with(&custom_path.item, cwd, true)
|
Ok(nu_path::expand_path_with(&custom_path.item, cwd, true))
|
||||||
} else {
|
} else {
|
||||||
engine_state
|
engine_state
|
||||||
.plugin_path
|
.plugin_path
|
||||||
@ -28,8 +27,53 @@ pub(crate) fn modify_plugin_file(
|
|||||||
span: Some(span),
|
span: Some(span),
|
||||||
help: Some("you may be running `nu` with --no-config-file".into()),
|
help: Some("you may be running `nu` with --no-config-file".into()),
|
||||||
inner: vec![],
|
inner: vec![],
|
||||||
})?
|
})
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_plugin_file(
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
span: Span,
|
||||||
|
custom_path: &Option<Spanned<String>>,
|
||||||
|
) -> Result<PluginRegistryFile, ShellError> {
|
||||||
|
let plugin_registry_file_path =
|
||||||
|
get_plugin_registry_file_path(engine_state, stack, span, custom_path)?;
|
||||||
|
|
||||||
|
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
|
||||||
|
|
||||||
|
// Try to read the plugin file if it exists
|
||||||
|
if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) {
|
||||||
|
PluginRegistryFile::read_from(
|
||||||
|
File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
|
||||||
|
msg: format!(
|
||||||
|
"failed to read `{}`: {}",
|
||||||
|
plugin_registry_file_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
span: file_span,
|
||||||
|
})?,
|
||||||
|
Some(file_span),
|
||||||
|
)
|
||||||
|
} else if let Some(path) = custom_path {
|
||||||
|
Err(ShellError::FileNotFound {
|
||||||
|
file: path.item.clone(),
|
||||||
|
span: path.span,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(PluginRegistryFile::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn modify_plugin_file(
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
span: Span,
|
||||||
|
custom_path: &Option<Spanned<String>>,
|
||||||
|
operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
let plugin_registry_file_path =
|
||||||
|
get_plugin_registry_file_path(engine_state, stack, span, custom_path)?;
|
||||||
|
|
||||||
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
|
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
|
||||||
|
|
||||||
@ -91,18 +135,24 @@ pub(crate) fn get_plugin_dirs(
|
|||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
) -> impl Iterator<Item = String> {
|
) -> impl Iterator<Item = String> {
|
||||||
// Get the NU_PLUGIN_DIRS constant or env var
|
// Get the NU_PLUGIN_DIRS from the constant and/or env var
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
let working_set = StateWorkingSet::new(engine_state);
|
||||||
let value = working_set
|
let dirs_from_const = working_set
|
||||||
.find_variable(b"$NU_PLUGIN_DIRS")
|
.find_variable(b"$NU_PLUGIN_DIRS")
|
||||||
.and_then(|var_id| working_set.get_constant(var_id).ok())
|
.and_then(|var_id| working_set.get_constant(var_id).ok())
|
||||||
.or_else(|| stack.get_env_var(engine_state, "NU_PLUGIN_DIRS"))
|
.cloned() // TODO: avoid this clone
|
||||||
.cloned(); // TODO: avoid this clone
|
|
||||||
|
|
||||||
// Get all of the strings in the list, if possible
|
|
||||||
value
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|value| value.into_list().ok())
|
.flat_map(|value| value.into_list().ok())
|
||||||
.flatten()
|
.flatten()
|
||||||
.flat_map(|list_item| list_item.coerce_into_string().ok())
|
.flat_map(|list_item| list_item.coerce_into_string().ok());
|
||||||
|
|
||||||
|
let dirs_from_env = stack
|
||||||
|
.get_env_var(engine_state, "NU_PLUGIN_DIRS")
|
||||||
|
.cloned() // TODO: avoid this clone
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|value| value.into_list().ok())
|
||||||
|
.flatten()
|
||||||
|
.flat_map(|list_item| list_item.coerce_into_string().ok());
|
||||||
|
|
||||||
|
dirs_from_const.chain(dirs_from_env)
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-color-confi
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-color-config"
|
name = "nu-color-config"
|
||||||
version = "0.99.1"
|
version = "0.101.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
bench = false
|
bench = false
|
||||||
@ -14,12 +14,12 @@ bench = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
nu-protocol = { path = "../nu-protocol", version = "0.101.0", default-features = false }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
nu-engine = { path = "../nu-engine", version = "0.101.0", default-features = false }
|
||||||
nu-json = { path = "../nu-json", version = "0.99.1" }
|
nu-json = { path = "../nu-json", version = "0.101.0" }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.99.1" }
|
nu-test-support = { path = "../nu-test-support", version = "0.101.0" }
|
@ -5,7 +5,6 @@ use nu_protocol::{Config, Value};
|
|||||||
// The default colors for shapes, used when there is no config for them.
|
// The default colors for shapes, used when there is no config for them.
|
||||||
pub fn default_shape_color(shape: &str) -> Style {
|
pub fn default_shape_color(shape: &str) -> Style {
|
||||||
match shape {
|
match shape {
|
||||||
"shape_and" => Style::new().fg(Color::Purple).bold(),
|
|
||||||
"shape_binary" => Style::new().fg(Color::Purple).bold(),
|
"shape_binary" => Style::new().fg(Color::Purple).bold(),
|
||||||
"shape_block" => Style::new().fg(Color::Blue).bold(),
|
"shape_block" => Style::new().fg(Color::Blue).bold(),
|
||||||
"shape_bool" => Style::new().fg(Color::LightCyan),
|
"shape_bool" => Style::new().fg(Color::LightCyan),
|
||||||
@ -30,7 +29,6 @@ pub fn default_shape_color(shape: &str) -> Style {
|
|||||||
"shape_match_pattern" => Style::new().fg(Color::Green),
|
"shape_match_pattern" => Style::new().fg(Color::Green),
|
||||||
"shape_nothing" => Style::new().fg(Color::LightCyan),
|
"shape_nothing" => Style::new().fg(Color::LightCyan),
|
||||||
"shape_operator" => Style::new().fg(Color::Yellow),
|
"shape_operator" => Style::new().fg(Color::Yellow),
|
||||||
"shape_or" => Style::new().fg(Color::Purple).bold(),
|
|
||||||
"shape_pipe" => Style::new().fg(Color::Purple).bold(),
|
"shape_pipe" => Style::new().fg(Color::Purple).bold(),
|
||||||
"shape_range" => Style::new().fg(Color::Yellow).bold(),
|
"shape_range" => Style::new().fg(Color::Yellow).bold(),
|
||||||
"shape_raw_string" => Style::new().fg(Color::LightMagenta).bold(),
|
"shape_raw_string" => Style::new().fg(Color::LightMagenta).bold(),
|
||||||
|
@ -223,7 +223,7 @@ fn test_computable_style_closure_basic() {
|
|||||||
];
|
];
|
||||||
let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
|
let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
|
||||||
assert_eq!(actual_repl.err, "");
|
assert_eq!(actual_repl.err, "");
|
||||||
assert_eq!(actual_repl.out, "[bell.obj, book.obj, candle.obj]");
|
assert_eq!(actual_repl.out, r#"["bell.obj", "book.obj", "candle.obj"]"#);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-command"
|
name = "nu-command"
|
||||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
|
||||||
version = "0.99.1"
|
version = "0.101.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -16,21 +16,21 @@ bench = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.99.1" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.101.0" }
|
||||||
nu-color-config = { path = "../nu-color-config", version = "0.99.1" }
|
nu-color-config = { path = "../nu-color-config", version = "0.101.0" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
nu-engine = { path = "../nu-engine", version = "0.101.0", default-features = false }
|
||||||
nu-glob = { path = "../nu-glob", version = "0.99.1" }
|
nu-glob = { path = "../nu-glob", version = "0.101.0" }
|
||||||
nu-json = { path = "../nu-json", version = "0.99.1" }
|
nu-json = { path = "../nu-json", version = "0.101.0" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
nu-parser = { path = "../nu-parser", version = "0.101.0" }
|
||||||
nu-path = { path = "../nu-path", version = "0.99.1" }
|
nu-path = { path = "../nu-path", version = "0.101.0" }
|
||||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.99.1" }
|
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.101.0" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
nu-protocol = { path = "../nu-protocol", version = "0.101.0", default-features = false }
|
||||||
nu-system = { path = "../nu-system", version = "0.99.1" }
|
nu-system = { path = "../nu-system", version = "0.101.0" }
|
||||||
nu-table = { path = "../nu-table", version = "0.99.1" }
|
nu-table = { path = "../nu-table", version = "0.101.0" }
|
||||||
nu-term-grid = { path = "../nu-term-grid", version = "0.99.1" }
|
nu-term-grid = { path = "../nu-term-grid", version = "0.101.0" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.99.1" }
|
nu-utils = { path = "../nu-utils", version = "0.101.0", default-features = false }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
nuon = { path = "../nuon", version = "0.99.1" }
|
nuon = { path = "../nuon", version = "0.101.0" }
|
||||||
|
|
||||||
alphanumeric-sort = { workspace = true }
|
alphanumeric-sort = { workspace = true }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
@ -43,7 +43,7 @@ chardetng = { workspace = true }
|
|||||||
chrono = { workspace = true, features = ["std", "unstable-locales", "clock"], default-features = false }
|
chrono = { workspace = true, features = ["std", "unstable-locales", "clock"], default-features = false }
|
||||||
chrono-humanize = { workspace = true }
|
chrono-humanize = { workspace = true }
|
||||||
chrono-tz = { workspace = true }
|
chrono-tz = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true, optional = true }
|
||||||
csv = { workspace = true }
|
csv = { workspace = true }
|
||||||
dialoguer = { workspace = true, default-features = false, features = ["fuzzy-select"] }
|
dialoguer = { workspace = true, default-features = false, features = ["fuzzy-select"] }
|
||||||
digest = { workspace = true, default-features = false }
|
digest = { workspace = true, default-features = false }
|
||||||
@ -61,24 +61,26 @@ lscolors = { workspace = true, default-features = false, features = ["nu-ansi-te
|
|||||||
md5 = { workspace = true }
|
md5 = { workspace = true }
|
||||||
mime = { workspace = true }
|
mime = { workspace = true }
|
||||||
mime_guess = { workspace = true }
|
mime_guess = { workspace = true }
|
||||||
multipart-rs = { workspace = true }
|
multipart-rs = { workspace = true, optional = true }
|
||||||
native-tls = { workspace = true }
|
native-tls = { workspace = true, optional = true }
|
||||||
notify-debouncer-full = { workspace = true, default-features = false }
|
notify-debouncer-full = { workspace = true, default-features = false, optional = true }
|
||||||
num-format = { workspace = true }
|
num-format = { workspace = true }
|
||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
oem_cp = { workspace = true }
|
||||||
open = { workspace = true }
|
open = { workspace = true, optional = true }
|
||||||
os_pipe = { workspace = true }
|
os_pipe = { workspace = true, optional = true }
|
||||||
pathdiff = { workspace = true }
|
pathdiff = { workspace = true }
|
||||||
percent-encoding = { workspace = true }
|
percent-encoding = { workspace = true }
|
||||||
print-positions = { workspace = true }
|
print-positions = { workspace = true }
|
||||||
quick-xml = { workspace = true }
|
quick-xml = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true, optional = true }
|
||||||
|
getrandom = { workspace = true, optional = true }
|
||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
roxmltree = { workspace = true }
|
roxmltree = { workspace = true }
|
||||||
rusqlite = { workspace = true, features = ["bundled", "backup", "chrono"], optional = true }
|
rusqlite = { workspace = true, features = ["bundled", "backup", "chrono"], optional = true }
|
||||||
rmp = { workspace = true }
|
rmp = { workspace = true }
|
||||||
|
scopeguard = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||||
serde_urlencoded = { workspace = true }
|
serde_urlencoded = { workspace = true }
|
||||||
@ -86,29 +88,29 @@ serde_yaml = { workspace = true }
|
|||||||
sha2 = { workspace = true }
|
sha2 = { workspace = true }
|
||||||
sysinfo = { workspace = true }
|
sysinfo = { workspace = true }
|
||||||
tabled = { workspace = true, features = ["ansi"], default-features = false }
|
tabled = { workspace = true, features = ["ansi"], default-features = false }
|
||||||
terminal_size = { workspace = true }
|
|
||||||
titlecase = { workspace = true }
|
titlecase = { workspace = true }
|
||||||
toml = { workspace = true, features = ["preserve_order"] }
|
toml = { workspace = true, features = ["preserve_order"] }
|
||||||
unicode-segmentation = { workspace = true }
|
unicode-segmentation = { workspace = true }
|
||||||
ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json", "native-tls"] }
|
ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json"] }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
uu_cp = { workspace = true }
|
uu_cp = { workspace = true, optional = true }
|
||||||
uu_mkdir = { workspace = true }
|
uu_mkdir = { workspace = true, optional = true }
|
||||||
uu_mktemp = { workspace = true }
|
uu_mktemp = { workspace = true, optional = true }
|
||||||
uu_mv = { workspace = true }
|
uu_mv = { workspace = true, optional = true }
|
||||||
uu_uname = { workspace = true }
|
uu_touch = { workspace = true, optional = true }
|
||||||
uu_whoami = { workspace = true }
|
uu_uname = { workspace = true, optional = true }
|
||||||
uuid = { workspace = true, features = ["v4"] }
|
uu_whoami = { workspace = true, optional = true }
|
||||||
|
uuid = { workspace = true, features = ["v4"], optional = true }
|
||||||
v_htmlescape = { workspace = true }
|
v_htmlescape = { workspace = true }
|
||||||
wax = { workspace = true }
|
wax = { workspace = true }
|
||||||
which = { workspace = true }
|
which = { workspace = true, optional = true }
|
||||||
unicode-width = { workspace = true }
|
unicode-width = { workspace = true }
|
||||||
data-encoding = { version = "2.6.0", features = ["alloc"] }
|
data-encoding = { version = "2.6.0", features = ["alloc"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = { workspace = true }
|
winreg = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(all(not(windows), not(target_arch = "wasm32")))'.dependencies]
|
||||||
uucore = { workspace = true, features = ["mode"] }
|
uucore = { workspace = true, features = ["mode"] }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
@ -134,13 +136,59 @@ features = [
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
plugin = ["nu-parser/plugin"]
|
default = ["os"]
|
||||||
|
os = [
|
||||||
|
# include other features
|
||||||
|
"js",
|
||||||
|
"network",
|
||||||
|
"nu-protocol/os",
|
||||||
|
"nu-utils/os",
|
||||||
|
|
||||||
|
# os-dependant dependencies
|
||||||
|
"crossterm",
|
||||||
|
"notify-debouncer-full",
|
||||||
|
"open",
|
||||||
|
"os_pipe",
|
||||||
|
"uu_cp",
|
||||||
|
"uu_mkdir",
|
||||||
|
"uu_mktemp",
|
||||||
|
"uu_mv",
|
||||||
|
"uu_touch",
|
||||||
|
"uu_uname",
|
||||||
|
"uu_whoami",
|
||||||
|
"which",
|
||||||
|
]
|
||||||
|
|
||||||
|
# The dependencies listed below need 'getrandom'.
|
||||||
|
# They work with JS (usually with wasm-bindgen) or regular OS support.
|
||||||
|
# Hence they are also put under the 'os' feature to avoid repetition.
|
||||||
|
js = [
|
||||||
|
"getrandom",
|
||||||
|
"getrandom/js",
|
||||||
|
"rand",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
# These dependencies require networking capabilities, especially the http
|
||||||
|
# interface requires openssl which is not easy to embed into wasm,
|
||||||
|
# using rustls could solve this issue.
|
||||||
|
network = [
|
||||||
|
"multipart-rs",
|
||||||
|
"native-tls",
|
||||||
|
"ureq/native-tls",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
plugin = [
|
||||||
|
"nu-parser/plugin",
|
||||||
|
"os",
|
||||||
|
]
|
||||||
sqlite = ["rusqlite"]
|
sqlite = ["rusqlite"]
|
||||||
trash-support = ["trash"]
|
trash-support = ["trash"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.99.1" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.101.0" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.99.1" }
|
nu-test-support = { path = "../nu-test-support", version = "0.101.0" }
|
||||||
|
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
mockito = { workspace = true, default-features = false }
|
mockito = { workspace = true, default-features = false }
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use nu_engine::{command_prelude::*, get_eval_expression};
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BytesBuild;
|
pub struct BytesBuild;
|
||||||
@ -49,8 +49,7 @@ impl Command for BytesBuild {
|
|||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
let eval_expression = get_eval_expression(engine_state);
|
for val in call.rest::<Value>(engine_state, stack, 0)? {
|
||||||
for val in call.rest_iter_flattened(engine_state, stack, eval_expression, 0)? {
|
|
||||||
let val_span = val.span();
|
let val_span = val.span();
|
||||||
match val {
|
match val {
|
||||||
Value::Binary { mut val, .. } => output.append(&mut val),
|
Value::Binary { mut val, .. } => output.append(&mut val),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use nu_protocol::{ShellError, Span, Value};
|
use nu_protocol::{Filesize, ShellError, Span, Value};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
/// A subset of [`Value`], which is hashable.
|
/// A subset of [`Value`], which is hashable.
|
||||||
@ -30,7 +30,7 @@ pub enum HashableValue {
|
|||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
Filesize {
|
Filesize {
|
||||||
val: i64,
|
val: Filesize,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
Duration {
|
Duration {
|
||||||
@ -198,7 +198,10 @@ mod test {
|
|||||||
(Value::int(1, span), HashableValue::Int { val: 1, span }),
|
(Value::int(1, span), HashableValue::Int { val: 1, span }),
|
||||||
(
|
(
|
||||||
Value::filesize(1, span),
|
Value::filesize(1, span),
|
||||||
HashableValue::Filesize { val: 1, span },
|
HashableValue::Filesize {
|
||||||
|
val: 1.into(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Value::duration(1, span),
|
Value::duration(1, span),
|
||||||
|
@ -167,7 +167,7 @@ fn fill(
|
|||||||
fn action(input: &Value, args: &Arguments, span: Span) -> Value {
|
fn action(input: &Value, args: &Arguments, span: Span) -> Value {
|
||||||
match input {
|
match input {
|
||||||
Value::Int { val, .. } => fill_int(*val, args, span),
|
Value::Int { val, .. } => fill_int(*val, args, span),
|
||||||
Value::Filesize { val, .. } => fill_int(*val, args, span),
|
Value::Filesize { val, .. } => fill_int(val.get(), args, span),
|
||||||
Value::Float { val, .. } => fill_float(*val, args, span),
|
Value::Float { val, .. } => fill_float(*val, args, span),
|
||||||
Value::String { val, .. } => fill_string(val, args, span),
|
Value::String { val, .. } => fill_string(val, args, span),
|
||||||
// Propagate errors by explicitly matching them before the final case.
|
// Propagate errors by explicitly matching them before the final case.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
pub struct Arguments {
|
struct Arguments {
|
||||||
cell_paths: Option<Vec<CellPath>>,
|
cell_paths: Option<Vec<CellPath>>,
|
||||||
compact: bool,
|
compact: bool,
|
||||||
}
|
}
|
||||||
@ -142,12 +142,12 @@ fn into_binary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn action(input: &Value, _args: &Arguments, span: Span) -> Value {
|
fn action(input: &Value, _args: &Arguments, span: Span) -> Value {
|
||||||
let value = match input {
|
let value = match input {
|
||||||
Value::Binary { .. } => input.clone(),
|
Value::Binary { .. } => input.clone(),
|
||||||
Value::Int { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span),
|
Value::Int { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span),
|
||||||
Value::Float { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span),
|
Value::Float { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span),
|
||||||
Value::Filesize { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span),
|
Value::Filesize { val, .. } => Value::binary(val.get().to_ne_bytes().to_vec(), span),
|
||||||
Value::String { val, .. } => Value::binary(val.as_bytes().to_vec(), span),
|
Value::String { val, .. } => Value::binary(val.as_bytes().to_vec(), span),
|
||||||
Value::Bool { val, .. } => Value::binary(i64::from(*val).to_ne_bytes().to_vec(), span),
|
Value::Bool { val, .. } => Value::binary(i64::from(*val).to_ne_bytes().to_vec(), span),
|
||||||
Value::Duration { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span),
|
Value::Duration { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{generate_strftime_list, parse_date_from_string};
|
use crate::{generate_strftime_list, parse_date_from_string};
|
||||||
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc};
|
||||||
use human_date_parser::{from_human_time, ParseResult};
|
use human_date_parser::{from_human_time, ParseResult};
|
||||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
@ -185,11 +185,13 @@ impl Command for SubCommand {
|
|||||||
example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
|
example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
|
||||||
#[allow(clippy::inconsistent_digit_grouping)]
|
#[allow(clippy::inconsistent_digit_grouping)]
|
||||||
result: Some(Value::date(
|
result: Some(Value::date(
|
||||||
DateTime::from_naive_utc_and_offset(
|
Local
|
||||||
NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
.from_local_datetime(
|
||||||
|
&NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
||||||
.expect("date calculation should not fail in test"),
|
.expect("date calculation should not fail in test"),
|
||||||
*Local::now().offset(),
|
)
|
||||||
),
|
.unwrap()
|
||||||
|
.with_timezone(Local::now().offset()),
|
||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
@ -275,12 +277,13 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
if let Ok(date) = from_human_time(&input_val) {
|
if let Ok(date) = from_human_time(&input_val) {
|
||||||
match date {
|
match date {
|
||||||
ParseResult::Date(date) => {
|
ParseResult::Date(date) => {
|
||||||
let time = NaiveTime::from_hms_opt(0, 0, 0).expect("valid time");
|
let time = Local::now().time();
|
||||||
let combined = date.and_time(time);
|
let combined = date.and_time(time);
|
||||||
let dt_fixed = DateTime::from_naive_utc_and_offset(
|
let local_offset = *Local::now().offset();
|
||||||
combined,
|
let dt_fixed =
|
||||||
*Local::now().offset(),
|
TimeZone::from_local_datetime(&local_offset, &combined)
|
||||||
);
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
return Value::date(dt_fixed, span);
|
return Value::date(dt_fixed, span);
|
||||||
}
|
}
|
||||||
ParseResult::DateTime(date) => {
|
ParseResult::DateTime(date) => {
|
||||||
@ -289,10 +292,11 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
ParseResult::Time(time) => {
|
ParseResult::Time(time) => {
|
||||||
let date = Local::now().date_naive();
|
let date = Local::now().date_naive();
|
||||||
let combined = date.and_time(time);
|
let combined = date.and_time(time);
|
||||||
let dt_fixed = DateTime::from_naive_utc_and_offset(
|
let local_offset = *Local::now().offset();
|
||||||
combined,
|
let dt_fixed =
|
||||||
*Local::now().offset(),
|
TimeZone::from_local_datetime(&local_offset, &combined)
|
||||||
);
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
return Value::date(dt_fixed, span);
|
return Value::date(dt_fixed, span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -386,13 +390,15 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
Ok(d) => Value::date ( d, head ),
|
Ok(d) => Value::date ( d, head ),
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
match NaiveDateTime::parse_from_str(val, &dt.0) {
|
match NaiveDateTime::parse_from_str(val, &dt.0) {
|
||||||
Ok(d) => Value::date (
|
Ok(d) => {
|
||||||
DateTime::from_naive_utc_and_offset(
|
let local_offset = *Local::now().offset();
|
||||||
d,
|
let dt_fixed =
|
||||||
*Local::now().offset(),
|
TimeZone::from_local_datetime(&local_offset, &d)
|
||||||
),
|
.single()
|
||||||
head,
|
.unwrap_or_default();
|
||||||
),
|
|
||||||
|
Value::date (dt_fixed,head)
|
||||||
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
Value::error (
|
Value::error (
|
||||||
ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) },
|
ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) },
|
||||||
@ -503,7 +509,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore]
|
||||||
fn takes_a_date_format_without_timezone() {
|
fn takes_a_date_format_without_timezone() {
|
||||||
|
// Ignoring this test for now because we changed the human-date-parser to use
|
||||||
|
// the users timezone instead of UTC. We may continue to tweak this behavior.
|
||||||
|
// Another hacky solution is to set the timezone to UTC in the test, which works
|
||||||
|
// on MacOS and Linux but hasn't been tested on Windows. Plus it kind of defeats
|
||||||
|
// the purpose of a "without_timezone" test.
|
||||||
|
// std::env::set_var("TZ", "UTC");
|
||||||
let date_str = Value::test_string("16.11.1984 8:00 am");
|
let date_str = Value::test_string("16.11.1984 8:00 am");
|
||||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()));
|
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()));
|
||||||
let args = Arguments {
|
let args = Arguments {
|
||||||
@ -513,12 +526,16 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let actual = action(&date_str, &args, Span::test_data());
|
let actual = action(&date_str, &args, Span::test_data());
|
||||||
let expected = Value::date(
|
let expected = Value::date(
|
||||||
DateTime::from_naive_utc_and_offset(
|
Local
|
||||||
NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P").unwrap(),
|
.from_local_datetime(
|
||||||
*Local::now().offset(),
|
&NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
||||||
),
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(Local::now().offset()),
|
||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(actual, expected)
|
assert_eq!(actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ impl Command for SubCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
|
fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
|
||||||
let value_span = input.span();
|
let value_span = input.span();
|
||||||
match input {
|
match input {
|
||||||
Value::Filesize { .. } => input.clone(),
|
Value::Filesize { .. } => input.clone(),
|
||||||
|
@ -253,13 +253,13 @@ fn action(input: &Value, args: &Arguments, span: Span) -> Value {
|
|||||||
convert_int(input, span, radix)
|
convert_int(input, span, radix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::Filesize { val, .. } => Value::int(*val, span),
|
Value::Filesize { val, .. } => Value::int(val.get(), span),
|
||||||
Value::Float { val, .. } => Value::int(
|
Value::Float { val, .. } => Value::int(
|
||||||
{
|
{
|
||||||
if radix == 10 {
|
if radix == 10 {
|
||||||
*val as i64
|
*val as i64
|
||||||
} else {
|
} else {
|
||||||
match convert_int(&Value::int(*val as i64, span), span, radix).as_i64() {
|
match convert_int(&Value::int(*val as i64, span), span, radix).as_int() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
_ => {
|
_ => {
|
||||||
return Value::error(
|
return Value::error(
|
||||||
|
@ -99,6 +99,11 @@ impl Command for SubCommand {
|
|||||||
"timezone" => Value::test_string("+02:00"),
|
"timezone" => Value::test_string("+02:00"),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "convert date components to table columns",
|
||||||
|
example: "2020-04-12T22:10:57+02:00 | into record | transpose | transpose -r",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use crate::parse_date_from_string;
|
use crate::parse_date_from_string;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::PipelineIterator;
|
use nu_protocol::PipelineIterator;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::{Regex, RegexBuilder};
|
use regex::{Regex, RegexBuilder};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct IntoValue;
|
pub struct IntoValue;
|
||||||
@ -18,7 +18,7 @@ impl Command for IntoValue {
|
|||||||
.input_output_types(vec![(Type::table(), Type::table())])
|
.input_output_types(vec![(Type::table(), Type::table())])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table(vec![]),
|
SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
||||||
"list of columns to update",
|
"list of columns to update",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
@ -271,8 +271,9 @@ const DATETIME_DMY_PATTERN: &str = r#"(?x)
|
|||||||
$
|
$
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
static DATETIME_DMY_RE: Lazy<Regex> =
|
static DATETIME_DMY_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Lazy::new(|| Regex::new(DATETIME_DMY_PATTERN).expect("datetime_dmy_pattern should be valid"));
|
Regex::new(DATETIME_DMY_PATTERN).expect("datetime_dmy_pattern should be valid")
|
||||||
|
});
|
||||||
const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
||||||
^
|
^
|
||||||
['"]? # optional quotes
|
['"]? # optional quotes
|
||||||
@ -297,8 +298,9 @@ const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
|||||||
['"]? # optional quotes
|
['"]? # optional quotes
|
||||||
$
|
$
|
||||||
"#;
|
"#;
|
||||||
static DATETIME_YMD_RE: Lazy<Regex> =
|
static DATETIME_YMD_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Lazy::new(|| Regex::new(DATETIME_YMD_PATTERN).expect("datetime_ymd_pattern should be valid"));
|
Regex::new(DATETIME_YMD_PATTERN).expect("datetime_ymd_pattern should be valid")
|
||||||
|
});
|
||||||
//2023-03-24 16:44:17.865147299 -05:00
|
//2023-03-24 16:44:17.865147299 -05:00
|
||||||
const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
||||||
^
|
^
|
||||||
@ -331,23 +333,24 @@ const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
|||||||
['"]? # optional quotes
|
['"]? # optional quotes
|
||||||
$
|
$
|
||||||
"#;
|
"#;
|
||||||
static DATETIME_YMDZ_RE: Lazy<Regex> =
|
static DATETIME_YMDZ_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Lazy::new(|| Regex::new(DATETIME_YMDZ_PATTERN).expect("datetime_ymdz_pattern should be valid"));
|
Regex::new(DATETIME_YMDZ_PATTERN).expect("datetime_ymdz_pattern should be valid")
|
||||||
|
});
|
||||||
|
|
||||||
static FLOAT_RE: Lazy<Regex> = Lazy::new(|| {
|
static FLOAT_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Regex::new(r"^\s*[-+]?((\d*\.\d+)([eE][-+]?\d+)?|inf|NaN|(\d+)[eE][-+]?\d+|\d+\.)$")
|
Regex::new(r"^\s*[-+]?((\d*\.\d+)([eE][-+]?\d+)?|inf|NaN|(\d+)[eE][-+]?\d+|\d+\.)$")
|
||||||
.expect("float pattern should be valid")
|
.expect("float pattern should be valid")
|
||||||
});
|
});
|
||||||
|
|
||||||
static INTEGER_RE: Lazy<Regex> =
|
static INTEGER_RE: LazyLock<Regex> =
|
||||||
Lazy::new(|| Regex::new(r"^\s*-?(\d+)$").expect("integer pattern should be valid"));
|
LazyLock::new(|| Regex::new(r"^\s*-?(\d+)$").expect("integer pattern should be valid"));
|
||||||
|
|
||||||
static INTEGER_WITH_DELIMS_RE: Lazy<Regex> = Lazy::new(|| {
|
static INTEGER_WITH_DELIMS_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Regex::new(r"^\s*-?(\d{1,3}([,_]\d{3})+)$")
|
Regex::new(r"^\s*-?(\d{1,3}([,_]\d{3})+)$")
|
||||||
.expect("integer with delimiters pattern should be valid")
|
.expect("integer with delimiters pattern should be valid")
|
||||||
});
|
});
|
||||||
|
|
||||||
static BOOLEAN_RE: Lazy<Regex> = Lazy::new(|| {
|
static BOOLEAN_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
RegexBuilder::new(r"^\s*(true)$|^(false)$")
|
RegexBuilder::new(r"^\s*(true)$|^(false)$")
|
||||||
.case_insensitive(true)
|
.case_insensitive(true)
|
||||||
.build()
|
.build()
|
||||||
|
@ -359,7 +359,6 @@ fn nu_value_to_sqlite_type(val: &Value) -> Result<&'static str, ShellError> {
|
|||||||
| Type::Custom(_)
|
| Type::Custom(_)
|
||||||
| Type::Error
|
| Type::Error
|
||||||
| Type::List(_)
|
| Type::List(_)
|
||||||
| Type::ListStream
|
|
||||||
| Type::Range
|
| Type::Range
|
||||||
| Type::Record(_)
|
| Type::Record(_)
|
||||||
| Type::Signature
|
| Type::Signature
|
||||||
|
@ -421,7 +421,7 @@ pub fn value_to_sql(value: Value) -> Result<Box<dyn rusqlite::ToSql>, ShellError
|
|||||||
Value::Bool { val, .. } => Box::new(val),
|
Value::Bool { val, .. } => Box::new(val),
|
||||||
Value::Int { val, .. } => Box::new(val),
|
Value::Int { val, .. } => Box::new(val),
|
||||||
Value::Float { val, .. } => Box::new(val),
|
Value::Float { val, .. } => Box::new(val),
|
||||||
Value::Filesize { val, .. } => Box::new(val),
|
Value::Filesize { val, .. } => Box::new(val.get()),
|
||||||
Value::Duration { val, .. } => Box::new(val),
|
Value::Duration { val, .. } => Box::new(val),
|
||||||
Value::Date { val, .. } => Box::new(val),
|
Value::Date { val, .. } => Box::new(val),
|
||||||
Value::String { val, .. } => Box::new(val),
|
Value::String { val, .. } => Box::new(val),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::date::utils::parse_date_from_string;
|
use crate::date::utils::parse_date_from_string;
|
||||||
use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
|
use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{report_parse_warning, ParseWarning};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SubCommand;
|
pub struct SubCommand;
|
||||||
@ -17,7 +18,7 @@ impl Command for SubCommand {
|
|||||||
(Type::String, Type::record()),
|
(Type::String, Type::record()),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032
|
.allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032
|
||||||
.category(Category::Date)
|
.category(Category::Deprecated)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
@ -35,6 +36,17 @@ impl Command for SubCommand {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
|
report_parse_warning(
|
||||||
|
&StateWorkingSet::new(engine_state),
|
||||||
|
&ParseWarning::DeprecatedWarning {
|
||||||
|
old_command: "date to-record".into(),
|
||||||
|
new_suggestion: "see `into record` command examples".into(),
|
||||||
|
span: head,
|
||||||
|
url: "`help into record`".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
// This doesn't match explicit nulls
|
// This doesn't match explicit nulls
|
||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::date::utils::parse_date_from_string;
|
use crate::date::utils::parse_date_from_string;
|
||||||
use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
|
use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{report_parse_warning, ParseWarning};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SubCommand;
|
pub struct SubCommand;
|
||||||
@ -17,7 +18,7 @@ impl Command for SubCommand {
|
|||||||
(Type::String, Type::table()),
|
(Type::String, Type::table()),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032
|
.allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032
|
||||||
.category(Category::Date)
|
.category(Category::Deprecated)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
@ -36,6 +37,16 @@ impl Command for SubCommand {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
|
report_parse_warning(
|
||||||
|
&StateWorkingSet::new(engine_state),
|
||||||
|
&ParseWarning::DeprecatedWarning {
|
||||||
|
old_command: "date to-table".into(),
|
||||||
|
new_suggestion: "see `into record` command examples".into(),
|
||||||
|
span: head,
|
||||||
|
url: "`help into record`".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// This doesn't match explicit nulls
|
// This doesn't match explicit nulls
|
||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_parser::parse;
|
use nu_parser::{flatten_block, parse};
|
||||||
use nu_protocol::engine::StateWorkingSet;
|
use nu_protocol::{engine::StateWorkingSet, record};
|
||||||
|
use serde_json::{json, Value as JsonValue};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Ast;
|
pub struct Ast;
|
||||||
@ -16,18 +17,120 @@ impl Command for Ast {
|
|||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("ast")
|
Signature::build("ast")
|
||||||
.input_output_types(vec![(Type::String, Type::record())])
|
.input_output_types(vec![
|
||||||
|
(Type::Nothing, Type::table()),
|
||||||
|
(Type::Nothing, Type::record()),
|
||||||
|
(Type::Nothing, Type::String),
|
||||||
|
])
|
||||||
.required(
|
.required(
|
||||||
"pipeline",
|
"pipeline",
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
"The pipeline to print the ast for.",
|
"The pipeline to print the ast for.",
|
||||||
)
|
)
|
||||||
.switch("json", "serialize to json", Some('j'))
|
.switch("json", "Serialize to json", Some('j'))
|
||||||
.switch("minify", "minify the nuon or json output", Some('m'))
|
.switch("minify", "Minify the nuon or json output", Some('m'))
|
||||||
|
.switch("flatten", "An easier to read version of the ast", Some('f'))
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.category(Category::Debug)
|
.category(Category::Debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
description: "Print the ast of a string",
|
||||||
|
example: "ast 'hello'",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Print the ast of a pipeline",
|
||||||
|
example: "ast 'ls | where name =~ README'",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Print the ast of a pipeline with an error",
|
||||||
|
example: "ast 'for x in 1..10 { echo $x '",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description:
|
||||||
|
"Print the ast of a pipeline with an error, as json, in a nushell table",
|
||||||
|
example: "ast 'for x in 1..10 { echo $x ' --json | get block | from json",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Print the ast of a pipeline with an error, as json, minified",
|
||||||
|
example: "ast 'for x in 1..10 { echo $x ' --json --minify",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Print the ast of a string flattened",
|
||||||
|
example: r#"ast "'hello'" --flatten"#,
|
||||||
|
result: Some(Value::test_list(vec![Value::test_record(record! {
|
||||||
|
"content" => Value::test_string("'hello'"),
|
||||||
|
"shape" => Value::test_string("shape_string"),
|
||||||
|
"span" => Value::test_record(record! {
|
||||||
|
"start" => Value::test_int(0),
|
||||||
|
"end" => Value::test_int(7),}),
|
||||||
|
})])),
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Print the ast of a string flattened, as json, minified",
|
||||||
|
example: r#"ast "'hello'" --flatten --json --minify"#,
|
||||||
|
result: Some(Value::test_string(
|
||||||
|
r#"[{"content":"'hello'","shape":"shape_string","span":{"start":0,"end":7}}]"#,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Print the ast of a pipeline flattened",
|
||||||
|
example: r#"ast 'ls | sort-by type name -i' --flatten"#,
|
||||||
|
result: Some(Value::test_list(vec![
|
||||||
|
Value::test_record(record! {
|
||||||
|
"content" => Value::test_string("ls"),
|
||||||
|
"shape" => Value::test_string("shape_external"),
|
||||||
|
"span" => Value::test_record(record! {
|
||||||
|
"start" => Value::test_int(0),
|
||||||
|
"end" => Value::test_int(2),}),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"content" => Value::test_string("|"),
|
||||||
|
"shape" => Value::test_string("shape_pipe"),
|
||||||
|
"span" => Value::test_record(record! {
|
||||||
|
"start" => Value::test_int(3),
|
||||||
|
"end" => Value::test_int(4),}),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"content" => Value::test_string("sort-by"),
|
||||||
|
"shape" => Value::test_string("shape_internalcall"),
|
||||||
|
"span" => Value::test_record(record! {
|
||||||
|
"start" => Value::test_int(5),
|
||||||
|
"end" => Value::test_int(12),}),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"content" => Value::test_string("type"),
|
||||||
|
"shape" => Value::test_string("shape_string"),
|
||||||
|
"span" => Value::test_record(record! {
|
||||||
|
"start" => Value::test_int(13),
|
||||||
|
"end" => Value::test_int(17),}),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"content" => Value::test_string("name"),
|
||||||
|
"shape" => Value::test_string("shape_string"),
|
||||||
|
"span" => Value::test_record(record! {
|
||||||
|
"start" => Value::test_int(18),
|
||||||
|
"end" => Value::test_int(22),}),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"content" => Value::test_string("-i"),
|
||||||
|
"shape" => Value::test_string("shape_flag"),
|
||||||
|
"span" => Value::test_record(record! {
|
||||||
|
"start" => Value::test_int(23),
|
||||||
|
"end" => Value::test_int(25),}),
|
||||||
|
}),
|
||||||
|
])),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
&self,
|
&self,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
@ -38,19 +141,81 @@ impl Command for Ast {
|
|||||||
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
|
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||||
let to_json = call.has_flag(engine_state, stack, "json")?;
|
let to_json = call.has_flag(engine_state, stack, "json")?;
|
||||||
let minify = call.has_flag(engine_state, stack, "minify")?;
|
let minify = call.has_flag(engine_state, stack, "minify")?;
|
||||||
|
let flatten = call.has_flag(engine_state, stack, "flatten")?;
|
||||||
|
|
||||||
let mut working_set = StateWorkingSet::new(engine_state);
|
let mut working_set = StateWorkingSet::new(engine_state);
|
||||||
let block_output = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
|
let offset = working_set.next_span_start();
|
||||||
|
let parsed_block = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
|
||||||
|
|
||||||
|
if flatten {
|
||||||
|
let flat = flatten_block(&working_set, &parsed_block);
|
||||||
|
if to_json {
|
||||||
|
let mut json_val: JsonValue = json!([]);
|
||||||
|
for (span, shape) in flat {
|
||||||
|
let content =
|
||||||
|
String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
|
||||||
|
|
||||||
|
let json = json!(
|
||||||
|
{
|
||||||
|
"content": content,
|
||||||
|
"shape": shape.to_string(),
|
||||||
|
"span": {
|
||||||
|
"start": span.start.checked_sub(offset),
|
||||||
|
"end": span.end.checked_sub(offset),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
json_merge(&mut json_val, &json);
|
||||||
|
}
|
||||||
|
let json_string = if minify {
|
||||||
|
if let Ok(json_str) = serde_json::to_string(&json_val) {
|
||||||
|
json_str
|
||||||
|
} else {
|
||||||
|
"{}".to_string()
|
||||||
|
}
|
||||||
|
} else if let Ok(json_str) = serde_json::to_string_pretty(&json_val) {
|
||||||
|
json_str
|
||||||
|
} else {
|
||||||
|
"{}".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::string(json_string, pipeline.span).into_pipeline_data())
|
||||||
|
} else {
|
||||||
|
// let mut rec: Record = Record::new();
|
||||||
|
let mut rec = vec![];
|
||||||
|
for (span, shape) in flat {
|
||||||
|
let content =
|
||||||
|
String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
|
||||||
|
let each_rec = record! {
|
||||||
|
"content" => Value::test_string(content),
|
||||||
|
"shape" => Value::test_string(shape.to_string()),
|
||||||
|
"span" => Value::test_record(record!{
|
||||||
|
"start" => Value::test_int(match span.start.checked_sub(offset) {
|
||||||
|
Some(start) => start as i64,
|
||||||
|
None => 0
|
||||||
|
}),
|
||||||
|
"end" => Value::test_int(match span.end.checked_sub(offset) {
|
||||||
|
Some(end) => end as i64,
|
||||||
|
None => 0
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
rec.push(Value::test_record(each_rec));
|
||||||
|
}
|
||||||
|
Ok(Value::list(rec, pipeline.span).into_pipeline_data())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let error_output = working_set.parse_errors.first();
|
let error_output = working_set.parse_errors.first();
|
||||||
let block_span = match &block_output.span {
|
let block_span = match &parsed_block.span {
|
||||||
Some(span) => span,
|
Some(span) => span,
|
||||||
None => &pipeline.span,
|
None => &pipeline.span,
|
||||||
};
|
};
|
||||||
if to_json {
|
if to_json {
|
||||||
// Get the block as json
|
// Get the block as json
|
||||||
let serde_block_str = if minify {
|
let serde_block_str = if minify {
|
||||||
serde_json::to_string(&*block_output)
|
serde_json::to_string(&*parsed_block)
|
||||||
} else {
|
} else {
|
||||||
serde_json::to_string_pretty(&*block_output)
|
serde_json::to_string_pretty(&*parsed_block)
|
||||||
};
|
};
|
||||||
let block_json = match serde_block_str {
|
let block_json = match serde_block_str {
|
||||||
Ok(json) => json,
|
Ok(json) => json,
|
||||||
@ -59,7 +224,7 @@ impl Command for Ast {
|
|||||||
from_type: "block".to_string(),
|
from_type: "block".to_string(),
|
||||||
span: *block_span,
|
span: *block_span,
|
||||||
help: Some(format!(
|
help: Some(format!(
|
||||||
"Error: {e}\nCan't convert {block_output:?} to string"
|
"Error: {e}\nCan't convert {parsed_block:?} to string"
|
||||||
)),
|
)),
|
||||||
})?,
|
})?,
|
||||||
};
|
};
|
||||||
@ -94,9 +259,9 @@ impl Command for Ast {
|
|||||||
} else {
|
} else {
|
||||||
let block_value = Value::string(
|
let block_value = Value::string(
|
||||||
if minify {
|
if minify {
|
||||||
format!("{block_output:?}")
|
format!("{parsed_block:?}")
|
||||||
} else {
|
} else {
|
||||||
format!("{block_output:#?}")
|
format!("{parsed_block:#?}")
|
||||||
},
|
},
|
||||||
pipeline.span,
|
pipeline.span,
|
||||||
);
|
);
|
||||||
@ -118,36 +283,25 @@ impl Command for Ast {
|
|||||||
Ok(output_record.into_pipeline_data())
|
Ok(output_record.into_pipeline_data())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn json_merge(a: &mut JsonValue, b: &JsonValue) {
|
||||||
vec![
|
match (a, b) {
|
||||||
Example {
|
(JsonValue::Object(ref mut a), JsonValue::Object(b)) => {
|
||||||
description: "Print the ast of a string",
|
for (k, v) in b {
|
||||||
example: "ast 'hello'",
|
json_merge(a.entry(k).or_insert(JsonValue::Null), v);
|
||||||
result: None,
|
}
|
||||||
},
|
}
|
||||||
Example {
|
(JsonValue::Array(ref mut a), JsonValue::Array(b)) => {
|
||||||
description: "Print the ast of a pipeline",
|
a.extend(b.clone());
|
||||||
example: "ast 'ls | where name =~ README'",
|
}
|
||||||
result: None,
|
(JsonValue::Array(ref mut a), JsonValue::Object(b)) => {
|
||||||
},
|
a.extend([JsonValue::Object(b.clone())]);
|
||||||
Example {
|
}
|
||||||
description: "Print the ast of a pipeline with an error",
|
(a, b) => {
|
||||||
example: "ast 'for x in 1..10 { echo $x '",
|
*a = b.clone();
|
||||||
result: None,
|
}
|
||||||
},
|
|
||||||
Example {
|
|
||||||
description:
|
|
||||||
"Print the ast of a pipeline with an error, as json, in a nushell table",
|
|
||||||
example: "ast 'for x in 1..10 { echo $x ' --json | get block | from json",
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
Example {
|
|
||||||
description: "Print the ast of a pipeline with an error, as json, minified",
|
|
||||||
example: "ast 'for x in 1..10 { echo $x ' --json --minify",
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ fn all_columns(span: Span) -> Value {
|
|||||||
let environment = {
|
let environment = {
|
||||||
let mut env_rec = Record::new();
|
let mut env_rec = Record::new();
|
||||||
for val in p.environ() {
|
for val in p.environ() {
|
||||||
if let Some((key, value)) = val.split_once('=') {
|
if let Some((key, value)) = val.to_string_lossy().split_once('=') {
|
||||||
let is_env_var_a_list = {
|
let is_env_var_a_list = {
|
||||||
{
|
{
|
||||||
#[cfg(target_family = "windows")]
|
#[cfg(target_family = "windows")]
|
||||||
@ -146,8 +146,8 @@ fn all_columns(span: Span) -> Value {
|
|||||||
"root" => root,
|
"root" => root,
|
||||||
"cwd" => cwd,
|
"cwd" => cwd,
|
||||||
"exe_path" => exe_path,
|
"exe_path" => exe_path,
|
||||||
"command" => Value::string(p.cmd().join(" "), span),
|
"command" => Value::string(p.cmd().join(std::ffi::OsStr::new(" ")).to_string_lossy(), span),
|
||||||
"name" => Value::string(p.name(), span),
|
"name" => Value::string(p.name().to_string_lossy(), span),
|
||||||
"environment" => environment,
|
"environment" => environment,
|
||||||
},
|
},
|
||||||
span,
|
span,
|
||||||
@ -177,4 +177,9 @@ fn get_thread_id() -> u64 {
|
|||||||
{
|
{
|
||||||
nix::sys::pthread::pthread_self() as u64
|
nix::sys::pthread::pthread_self() as u64
|
||||||
}
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
// wasm doesn't have any threads accessible, so we return 0 as a fallback
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::inspect_table;
|
use super::inspect_table;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use terminal_size::{terminal_size, Height, Width};
|
use nu_utils::terminal_size;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Inspect;
|
pub struct Inspect;
|
||||||
@ -38,12 +38,9 @@ impl Command for Inspect {
|
|||||||
let original_input = input_val.clone();
|
let original_input = input_val.clone();
|
||||||
let description = input_val.get_type().to_string();
|
let description = input_val.get_type().to_string();
|
||||||
|
|
||||||
let (cols, _rows) = match terminal_size() {
|
let (cols, _rows) = terminal_size().unwrap_or((0, 0));
|
||||||
Some((w, h)) => (Width(w.0), Height(h.0)),
|
|
||||||
None => (Width(0), Height(0)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let table = inspect_table::build_table(input_val, description, cols.0 as usize);
|
let table = inspect_table::build_table(input_val, description, cols as usize);
|
||||||
|
|
||||||
// Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell
|
// Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell
|
||||||
// tabular output. If we printed to stdout, nushell would get confused with two outputs.
|
// tabular output. If we printed to stdout, nushell would get confused with two outputs.
|
||||||
|
@ -10,6 +10,7 @@ mod metadata_set;
|
|||||||
mod profile;
|
mod profile;
|
||||||
mod timeit;
|
mod timeit;
|
||||||
mod view;
|
mod view;
|
||||||
|
mod view_blocks;
|
||||||
mod view_files;
|
mod view_files;
|
||||||
mod view_ir;
|
mod view_ir;
|
||||||
mod view_source;
|
mod view_source;
|
||||||
@ -27,6 +28,7 @@ pub use metadata_set::MetadataSet;
|
|||||||
pub use profile::DebugProfile;
|
pub use profile::DebugProfile;
|
||||||
pub use timeit::TimeIt;
|
pub use timeit::TimeIt;
|
||||||
pub use view::View;
|
pub use view::View;
|
||||||
|
pub use view_blocks::ViewBlocks;
|
||||||
pub use view_files::ViewFiles;
|
pub use view_files::ViewFiles;
|
||||||
pub use view_ir::ViewIr;
|
pub use view_ir::ViewIr;
|
||||||
pub use view_source::ViewSource;
|
pub use view_source::ViewSource;
|
||||||
|
@ -30,8 +30,6 @@ impl Command for DebugProfile {
|
|||||||
"Collect pipeline element output values",
|
"Collect pipeline element output values",
|
||||||
Some('v'),
|
Some('v'),
|
||||||
)
|
)
|
||||||
.switch("expr", "Collect expression types", Some('x'))
|
|
||||||
.switch("instructions", "Collect IR instructions", Some('i'))
|
|
||||||
.switch("lines", "Collect line numbers", Some('l'))
|
.switch("lines", "Collect line numbers", Some('l'))
|
||||||
.named(
|
.named(
|
||||||
"max-depth",
|
"max-depth",
|
||||||
@ -48,37 +46,52 @@ impl Command for DebugProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extra_description(&self) -> &str {
|
fn extra_description(&self) -> &str {
|
||||||
r#"The profiler profiles every evaluated pipeline element inside a closure, stepping into all
|
r#"The profiler profiles every evaluated instruction inside a closure, stepping into all
|
||||||
commands calls and other blocks/closures.
|
commands calls and other blocks/closures.
|
||||||
|
|
||||||
The output can be heavily customized. By default, the following columns are included:
|
The output can be heavily customized. By default, the following columns are included:
|
||||||
- depth : Depth of the pipeline element. Each entered block adds one level of depth. How many
|
- depth : Depth of the instruction. Each entered block adds one level of depth. How many
|
||||||
blocks deep to step into is controlled with the --max-depth option.
|
blocks deep to step into is controlled with the --max-depth option.
|
||||||
- id : ID of the pipeline element
|
- id : ID of the instruction
|
||||||
- parent_id : ID of the parent element
|
- parent_id : ID of the instruction that created the parent scope
|
||||||
- source : Source code of the pipeline element. If the element has multiple lines, only the
|
- source : Source code that generated the instruction. If the source code has multiple lines,
|
||||||
first line is used and `...` is appended to the end. Full source code can be shown
|
only the first line is used and `...` is appended to the end. Full source code can
|
||||||
with the --expand-source flag.
|
be shown with the --expand-source flag.
|
||||||
- duration_ms : How long it took to run the pipeline element in milliseconds.
|
- pc : The index of the instruction within the block.
|
||||||
- (optional) span : Span of the element. Can be viewed via the `view span` command. Enabled with
|
- instruction : The pretty printed instruction being evaluated.
|
||||||
the --spans flag.
|
- duration_ms : How long it took to run the instruction in milliseconds.
|
||||||
- (optional) expr : The type of expression of the pipeline element. Enabled with the --expr flag.
|
- (optional) span : Span associated with the instruction. Can be viewed via the `view span`
|
||||||
- (optional) output : The output value of the pipeline element. Enabled with the --values flag.
|
command. Enabled with the --spans flag.
|
||||||
|
- (optional) output : The output value of the instruction. Enabled with the --values flag.
|
||||||
|
|
||||||
To illustrate the depth and IDs, consider `debug profile { if true { echo 'spam' } }`. There are
|
To illustrate the depth and IDs, consider `debug profile { do { if true { echo 'spam' } } }`. A unique ID is generated each time an instruction is executed, and there are two levels of depth:
|
||||||
three pipeline elements:
|
|
||||||
|
|
||||||
depth id parent_id
|
```
|
||||||
0 0 0 debug profile { do { if true { 'spam' } } }
|
depth id parent_id source pc instruction
|
||||||
1 1 0 if true { 'spam' }
|
0 0 0 debug profile { do { if true { 'spam' } } } 0 <start>
|
||||||
2 2 1 'spam'
|
1 1 0 { if true { 'spam' } } 0 load-literal %1, closure(2164)
|
||||||
|
1 2 0 { if true { 'spam' } } 1 push-positional %1
|
||||||
|
1 3 0 { do { if true { 'spam' } } } 2 redirect-out caller
|
||||||
|
1 4 0 { do { if true { 'spam' } } } 3 redirect-err caller
|
||||||
|
1 5 0 do 4 call decl 7 "do", %0
|
||||||
|
2 6 5 true 0 load-literal %1, bool(true)
|
||||||
|
2 7 5 if 1 not %1
|
||||||
|
2 8 5 if 2 branch-if %1, 5
|
||||||
|
2 9 5 'spam' 3 load-literal %0, string("spam")
|
||||||
|
2 10 5 if 4 jump 6
|
||||||
|
2 11 5 { if true { 'spam' } } 6 return %0
|
||||||
|
1 12 0 { do { if true { 'spam' } } } 5 return %0
|
||||||
|
```
|
||||||
|
|
||||||
Each block entered increments depth by 1 and each block left decrements it by one. This way you can
|
Each block entered increments depth by 1 and each block left decrements it by one. This way you can
|
||||||
control the profiling granularity. Passing --max-depth=1 to the above would stop at
|
control the profiling granularity. Passing --max-depth=1 to the above would stop inside the `do`
|
||||||
`if true { 'spam' }`. The id is used to identify each element. The parent_id tells you that 'spam'
|
at `if true { 'spam' }`. The id is used to identify each element. The parent_id tells you that the
|
||||||
was spawned from `if true { 'spam' }` which was spawned from the root `debug profile { ... }`.
|
instructions inside the block are being executed because of `do` (5), which in turn was spawned from
|
||||||
|
the root `debug profile { ... }`.
|
||||||
|
|
||||||
Note: In some cases, the ordering of piepeline elements might not be intuitive. For example,
|
For a better understanding of how instructions map to source code, see the `view ir` command.
|
||||||
|
|
||||||
|
Note: In some cases, the ordering of pipeline elements might not be intuitive. For example,
|
||||||
`[ a bb cc ] | each { $in | str length }` involves some implicit collects and lazy evaluation
|
`[ a bb cc ] | each { $in | str length }` involves some implicit collects and lazy evaluation
|
||||||
confusing the id/parent_id hierarchy. The --expr flag is helpful for investigating these issues."#
|
confusing the id/parent_id hierarchy. The --expr flag is helpful for investigating these issues."#
|
||||||
}
|
}
|
||||||
@ -94,8 +107,6 @@ confusing the id/parent_id hierarchy. The --expr flag is helpful for investigati
|
|||||||
let collect_spans = call.has_flag(engine_state, stack, "spans")?;
|
let collect_spans = call.has_flag(engine_state, stack, "spans")?;
|
||||||
let collect_expanded_source = call.has_flag(engine_state, stack, "expanded-source")?;
|
let collect_expanded_source = call.has_flag(engine_state, stack, "expanded-source")?;
|
||||||
let collect_values = call.has_flag(engine_state, stack, "values")?;
|
let collect_values = call.has_flag(engine_state, stack, "values")?;
|
||||||
let collect_exprs = call.has_flag(engine_state, stack, "expr")?;
|
|
||||||
let collect_instructions = call.has_flag(engine_state, stack, "instructions")?;
|
|
||||||
let collect_lines = call.has_flag(engine_state, stack, "lines")?;
|
let collect_lines = call.has_flag(engine_state, stack, "lines")?;
|
||||||
let max_depth = call
|
let max_depth = call
|
||||||
.get_flag(engine_state, stack, "max-depth")?
|
.get_flag(engine_state, stack, "max-depth")?
|
||||||
@ -108,8 +119,8 @@ confusing the id/parent_id hierarchy. The --expr flag is helpful for investigati
|
|||||||
collect_source: true,
|
collect_source: true,
|
||||||
collect_expanded_source,
|
collect_expanded_source,
|
||||||
collect_values,
|
collect_values,
|
||||||
collect_exprs,
|
collect_exprs: false,
|
||||||
collect_instructions,
|
collect_instructions: true,
|
||||||
collect_lines,
|
collect_lines,
|
||||||
},
|
},
|
||||||
call.span(),
|
call.span(),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression_with_input};
|
use nu_engine::{command_prelude::*, ClosureEvalOnce};
|
||||||
|
use nu_protocol::engine::Closure;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -10,16 +11,18 @@ impl Command for TimeIt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"Time the running time of a block."
|
"Time how long it takes a closure to run."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
"Any pipeline input given to this command is passed to the closure. Note that streaming inputs may affect timing results, and it is recommended to add a `collect` command before this if the input is a stream.
|
||||||
|
|
||||||
|
This command will bubble up any errors encountered when running the closure. The return pipeline of the closure is collected into a value and then discarded."
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature(&self) -> nu_protocol::Signature {
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
Signature::build("timeit")
|
Signature::build("timeit")
|
||||||
.required(
|
.required("command", SyntaxShape::Closure(None), "The closure to run.")
|
||||||
"command",
|
|
||||||
SyntaxShape::OneOf(vec![SyntaxShape::Block, SyntaxShape::Expression]),
|
|
||||||
"The command or block to run.",
|
|
||||||
)
|
|
||||||
.input_output_types(vec![
|
.input_output_types(vec![
|
||||||
(Type::Any, Type::Duration),
|
(Type::Any, Type::Duration),
|
||||||
(Type::Nothing, Type::Duration),
|
(Type::Nothing, Type::Duration),
|
||||||
@ -46,51 +49,38 @@ impl Command for TimeIt {
|
|||||||
// reset outdest, so the command can write to stdout and stderr.
|
// reset outdest, so the command can write to stdout and stderr.
|
||||||
let stack = &mut stack.push_redirection(None, None);
|
let stack = &mut stack.push_redirection(None, None);
|
||||||
|
|
||||||
let command_to_run = call.positional_nth(stack, 0);
|
let closure: Closure = call.req(engine_state, stack, 0)?;
|
||||||
|
let closure = ClosureEvalOnce::new_preserve_out_dest(engine_state, stack, closure);
|
||||||
|
|
||||||
// Get the start time after all other computation has been done.
|
// Get the start time after all other computation has been done.
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
closure.run_with_input(input)?.into_value(call.head)?;
|
||||||
|
let time = start_time.elapsed();
|
||||||
|
|
||||||
if let Some(command_to_run) = command_to_run {
|
let output = Value::duration(time.as_nanos() as i64, call.head);
|
||||||
if let Some(block_id) = command_to_run.as_block() {
|
|
||||||
let eval_block = get_eval_block(engine_state);
|
|
||||||
let block = engine_state.get_block(block_id);
|
|
||||||
eval_block(engine_state, stack, block, input)?
|
|
||||||
} else {
|
|
||||||
let eval_expression_with_input = get_eval_expression_with_input(engine_state);
|
|
||||||
let expression = &command_to_run.clone();
|
|
||||||
eval_expression_with_input(engine_state, stack, expression, input)?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PipelineData::empty()
|
|
||||||
}
|
|
||||||
.into_value(call.head)?;
|
|
||||||
|
|
||||||
let end_time = Instant::now();
|
|
||||||
|
|
||||||
let output = Value::duration(
|
|
||||||
end_time.saturating_duration_since(start_time).as_nanos() as i64,
|
|
||||||
call.head,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(output.into_pipeline_data())
|
Ok(output.into_pipeline_data())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
description: "Times a command within a closure",
|
description: "Time a closure containing one command",
|
||||||
example: "timeit { sleep 500ms }",
|
example: "timeit { sleep 500ms }",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Times a command using an existing input",
|
description: "Time a closure with an input value",
|
||||||
example: "http get https://www.nushell.sh/book/ | timeit { split chars }",
|
example: "'A really long string' | timeit { split chars }",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Times a command invocation",
|
description: "Time a closure with an input stream",
|
||||||
example: "timeit ls -la",
|
example: "open some_file.txt | collect | timeit { split chars }",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Time a closure containing a pipeline",
|
||||||
|
example: "timeit { open some_file.txt | split chars }",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
71
crates/nu-command/src/debug/view_blocks.rs
Normal file
71
crates/nu-command/src/debug/view_blocks.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ViewBlocks;
|
||||||
|
|
||||||
|
impl Command for ViewBlocks {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"view blocks"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"View the blocks registered in nushell's EngineState memory."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
"These are blocks parsed and loaded at runtime as well as any blocks that accumulate in the repl."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("view blocks")
|
||||||
|
.input_output_types(vec![(
|
||||||
|
Type::Nothing,
|
||||||
|
Type::Table(
|
||||||
|
[
|
||||||
|
("block_id".into(), Type::Int),
|
||||||
|
("content".into(), Type::String),
|
||||||
|
("start".into(), Type::Int),
|
||||||
|
("end".into(), Type::Int),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
)])
|
||||||
|
.category(Category::Debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let mut records = vec![];
|
||||||
|
|
||||||
|
for block_id in 0..engine_state.num_blocks() {
|
||||||
|
let block = engine_state.get_block(nu_protocol::BlockId::new(block_id));
|
||||||
|
|
||||||
|
if let Some(span) = block.span {
|
||||||
|
let contents_bytes = engine_state.get_span_contents(span);
|
||||||
|
let contents_string = String::from_utf8_lossy(contents_bytes);
|
||||||
|
let cur_rec = record! {
|
||||||
|
"block_id" => Value::int(block_id as i64, span),
|
||||||
|
"content" => Value::string(contents_string.trim().to_string(), span),
|
||||||
|
"start" => Value::int(span.start as i64, span),
|
||||||
|
"end" => Value::int(span.end as i64, span),
|
||||||
|
};
|
||||||
|
records.push(Value::record(cur_rec, span));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::list(records, call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![Example {
|
||||||
|
description: "View the blocks registered in Nushell's EngineState memory",
|
||||||
|
example: r#"view blocks"#,
|
||||||
|
result: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
@ -33,6 +33,34 @@ impl Command for ViewSource {
|
|||||||
let arg_span = arg.span();
|
let arg_span = arg.span();
|
||||||
|
|
||||||
let source = match arg {
|
let source = match arg {
|
||||||
|
Value::Int { val, .. } => {
|
||||||
|
if let Some(block) =
|
||||||
|
engine_state.try_get_block(nu_protocol::BlockId::new(val as usize))
|
||||||
|
{
|
||||||
|
if let Some(span) = block.span {
|
||||||
|
let contents = engine_state.get_span_contents(span);
|
||||||
|
Ok(Value::string(String::from_utf8_lossy(contents), call.head)
|
||||||
|
.into_pipeline_data())
|
||||||
|
} else {
|
||||||
|
Err(ShellError::GenericError {
|
||||||
|
error: "Cannot view int value".to_string(),
|
||||||
|
msg: "the block does not have a viewable span".to_string(),
|
||||||
|
span: Some(arg_span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(ShellError::GenericError {
|
||||||
|
error: format!("Block Id {} does not exist", arg.coerce_into_string()?),
|
||||||
|
msg: "this number does not correspond to a block".to_string(),
|
||||||
|
span: Some(arg_span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Value::String { val, .. } => {
|
Value::String { val, .. } => {
|
||||||
if let Some(decl_id) = engine_state.find_decl(val.as_bytes(), &[]) {
|
if let Some(decl_id) = engine_state.find_decl(val.as_bytes(), &[]) {
|
||||||
// arg is a command
|
// arg is a command
|
||||||
@ -130,7 +158,7 @@ impl Command for ViewSource {
|
|||||||
Ok(Value::string(final_contents, call.head).into_pipeline_data())
|
Ok(Value::string(final_contents, call.head).into_pipeline_data())
|
||||||
} else {
|
} else {
|
||||||
Err(ShellError::GenericError {
|
Err(ShellError::GenericError {
|
||||||
error: "Cannot view value".to_string(),
|
error: "Cannot view string value".to_string(),
|
||||||
msg: "the command does not have a viewable block span".to_string(),
|
msg: "the command does not have a viewable block span".to_string(),
|
||||||
span: Some(arg_span),
|
span: Some(arg_span),
|
||||||
help: None,
|
help: None,
|
||||||
@ -139,7 +167,7 @@ impl Command for ViewSource {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ShellError::GenericError {
|
Err(ShellError::GenericError {
|
||||||
error: "Cannot view value".to_string(),
|
error: "Cannot view string decl value".to_string(),
|
||||||
msg: "the command does not have a viewable block".to_string(),
|
msg: "the command does not have a viewable block".to_string(),
|
||||||
span: Some(arg_span),
|
span: Some(arg_span),
|
||||||
help: None,
|
help: None,
|
||||||
@ -155,7 +183,7 @@ impl Command for ViewSource {
|
|||||||
.into_pipeline_data())
|
.into_pipeline_data())
|
||||||
} else {
|
} else {
|
||||||
Err(ShellError::GenericError {
|
Err(ShellError::GenericError {
|
||||||
error: "Cannot view value".to_string(),
|
error: "Cannot view string module value".to_string(),
|
||||||
msg: "the module does not have a viewable block".to_string(),
|
msg: "the module does not have a viewable block".to_string(),
|
||||||
span: Some(arg_span),
|
span: Some(arg_span),
|
||||||
help: None,
|
help: None,
|
||||||
@ -164,7 +192,7 @@ impl Command for ViewSource {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ShellError::GenericError {
|
Err(ShellError::GenericError {
|
||||||
error: "Cannot view value".to_string(),
|
error: "Cannot view string value".to_string(),
|
||||||
msg: "this name does not correspond to a viewable value".to_string(),
|
msg: "this name does not correspond to a viewable value".to_string(),
|
||||||
span: Some(arg_span),
|
span: Some(arg_span),
|
||||||
help: None,
|
help: None,
|
||||||
|
@ -27,6 +27,10 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
|
#[cfg(feature = "rand")]
|
||||||
|
bind_command! {
|
||||||
|
Shuffle
|
||||||
|
}
|
||||||
bind_command! {
|
bind_command! {
|
||||||
All,
|
All,
|
||||||
Any,
|
Any,
|
||||||
@ -57,6 +61,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
SplitBy,
|
SplitBy,
|
||||||
Take,
|
Take,
|
||||||
Merge,
|
Merge,
|
||||||
|
MergeDeep,
|
||||||
Move,
|
Move,
|
||||||
TakeWhile,
|
TakeWhile,
|
||||||
TakeUntil,
|
TakeUntil,
|
||||||
@ -64,6 +69,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
Length,
|
Length,
|
||||||
Lines,
|
Lines,
|
||||||
ParEach,
|
ParEach,
|
||||||
|
ChunkBy,
|
||||||
Prepend,
|
Prepend,
|
||||||
Range,
|
Range,
|
||||||
Reduce,
|
Reduce,
|
||||||
@ -71,7 +77,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
Rename,
|
Rename,
|
||||||
Reverse,
|
Reverse,
|
||||||
Select,
|
Select,
|
||||||
Shuffle,
|
|
||||||
Skip,
|
Skip,
|
||||||
SkipUntil,
|
SkipUntil,
|
||||||
SkipWhile,
|
SkipWhile,
|
||||||
@ -102,6 +107,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
bind_command! {
|
bind_command! {
|
||||||
Path,
|
Path,
|
||||||
PathBasename,
|
PathBasename,
|
||||||
|
PathSelf,
|
||||||
PathDirname,
|
PathDirname,
|
||||||
PathExists,
|
PathExists,
|
||||||
PathExpand,
|
PathExpand,
|
||||||
@ -113,6 +119,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// System
|
// System
|
||||||
|
#[cfg(feature = "os")]
|
||||||
bind_command! {
|
bind_command! {
|
||||||
Complete,
|
Complete,
|
||||||
External,
|
External,
|
||||||
@ -154,16 +161,19 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
MetadataSet,
|
MetadataSet,
|
||||||
TimeIt,
|
TimeIt,
|
||||||
View,
|
View,
|
||||||
|
ViewBlocks,
|
||||||
ViewFiles,
|
ViewFiles,
|
||||||
ViewIr,
|
ViewIr,
|
||||||
ViewSource,
|
ViewSource,
|
||||||
ViewSpan,
|
ViewSpan,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(all(feature = "os", windows))]
|
||||||
bind_command! { RegistryQuery }
|
bind_command! { RegistryQuery }
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(all(
|
||||||
|
feature = "os",
|
||||||
|
any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
@ -171,6 +181,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
target_os = "openbsd",
|
target_os = "openbsd",
|
||||||
target_os = "macos",
|
target_os = "macos",
|
||||||
target_os = "windows"
|
target_os = "windows"
|
||||||
|
)
|
||||||
))]
|
))]
|
||||||
bind_command! { Ps };
|
bind_command! { Ps };
|
||||||
|
|
||||||
@ -218,6 +229,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// FileSystem
|
// FileSystem
|
||||||
|
#[cfg(feature = "os")]
|
||||||
bind_command! {
|
bind_command! {
|
||||||
Cd,
|
Cd,
|
||||||
Ls,
|
Ls,
|
||||||
@ -230,11 +242,13 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
Rm,
|
Rm,
|
||||||
Save,
|
Save,
|
||||||
Touch,
|
Touch,
|
||||||
|
UTouch,
|
||||||
Glob,
|
Glob,
|
||||||
Watch,
|
Watch,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Platform
|
// Platform
|
||||||
|
#[cfg(feature = "os")]
|
||||||
bind_command! {
|
bind_command! {
|
||||||
Ansi,
|
Ansi,
|
||||||
AnsiLink,
|
AnsiLink,
|
||||||
@ -247,11 +261,13 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
IsTerminal,
|
IsTerminal,
|
||||||
Kill,
|
Kill,
|
||||||
Sleep,
|
Sleep,
|
||||||
|
Term,
|
||||||
TermSize,
|
TermSize,
|
||||||
|
TermQuery,
|
||||||
Whoami,
|
Whoami,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(all(unix, feature = "os"))]
|
||||||
bind_command! { ULimit };
|
bind_command! { ULimit };
|
||||||
|
|
||||||
// Date
|
// Date
|
||||||
@ -335,6 +351,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
WithEnv,
|
WithEnv,
|
||||||
ConfigNu,
|
ConfigNu,
|
||||||
ConfigEnv,
|
ConfigEnv,
|
||||||
|
ConfigFlatten,
|
||||||
ConfigMeta,
|
ConfigMeta,
|
||||||
ConfigReset,
|
ConfigReset,
|
||||||
};
|
};
|
||||||
@ -376,6 +393,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
|
#[cfg(feature = "network")]
|
||||||
bind_command! {
|
bind_command! {
|
||||||
Http,
|
Http,
|
||||||
HttpDelete,
|
HttpDelete,
|
||||||
@ -385,16 +403,20 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
HttpPost,
|
HttpPost,
|
||||||
HttpPut,
|
HttpPut,
|
||||||
HttpOptions,
|
HttpOptions,
|
||||||
|
Port,
|
||||||
|
}
|
||||||
|
bind_command! {
|
||||||
Url,
|
Url,
|
||||||
UrlBuildQuery,
|
UrlBuildQuery,
|
||||||
|
UrlSplitQuery,
|
||||||
UrlDecode,
|
UrlDecode,
|
||||||
UrlEncode,
|
UrlEncode,
|
||||||
UrlJoin,
|
UrlJoin,
|
||||||
UrlParse,
|
UrlParse,
|
||||||
Port,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random
|
// Random
|
||||||
|
#[cfg(feature = "rand")]
|
||||||
bind_command! {
|
bind_command! {
|
||||||
Random,
|
Random,
|
||||||
RandomBool,
|
RandomBool,
|
||||||
|
80
crates/nu-command/src/env/config/config_.rs
vendored
80
crates/nu-command/src/env/config/config_.rs
vendored
@ -1,4 +1,6 @@
|
|||||||
use nu_engine::{command_prelude::*, get_full_help};
|
use nu_cmd_base::util::get_editor;
|
||||||
|
use nu_engine::{command_prelude::*, env_to_strings, get_full_help};
|
||||||
|
use nu_system::ForegroundChild;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigMeta;
|
pub struct ConfigMeta;
|
||||||
@ -36,3 +38,79 @@ impl Command for ConfigMeta {
|
|||||||
vec!["options", "setup"]
|
vec!["options", "setup"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "os"))]
|
||||||
|
pub(super) fn start_editor(
|
||||||
|
_: &'static str,
|
||||||
|
_: &EngineState,
|
||||||
|
_: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
Err(ShellError::DisabledOsSupport {
|
||||||
|
msg: "Running external commands is not available without OS support.".to_string(),
|
||||||
|
span: Some(call.head),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "os")]
|
||||||
|
pub(super) fn start_editor(
|
||||||
|
config_path: &'static str,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
// Find the editor executable.
|
||||||
|
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||||
|
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||||
|
let cwd = engine_state.cwd(Some(stack))?;
|
||||||
|
let editor_executable =
|
||||||
|
crate::which(&editor_name, &paths, cwd.as_ref()).ok_or(ShellError::ExternalCommand {
|
||||||
|
label: format!("`{editor_name}` not found"),
|
||||||
|
help: "Failed to find the editor executable".into(),
|
||||||
|
span: call.head,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(config_path) = engine_state.get_config_path(config_path) else {
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
|
error: format!("Could not find $nu.{config_path}"),
|
||||||
|
msg: format!("Could not find $nu.{config_path}"),
|
||||||
|
span: None,
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let config_path = config_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Create the command.
|
||||||
|
let mut command = std::process::Command::new(editor_executable);
|
||||||
|
|
||||||
|
// Configure PWD.
|
||||||
|
command.current_dir(cwd);
|
||||||
|
|
||||||
|
// Configure environment variables.
|
||||||
|
let envs = env_to_strings(engine_state, stack)?;
|
||||||
|
command.env_clear();
|
||||||
|
command.envs(envs);
|
||||||
|
|
||||||
|
// Configure args.
|
||||||
|
command.arg(config_path);
|
||||||
|
command.args(editor_args);
|
||||||
|
|
||||||
|
// Spawn the child process. On Unix, also put the child process to
|
||||||
|
// foreground if we're in an interactive session.
|
||||||
|
#[cfg(windows)]
|
||||||
|
let child = ForegroundChild::spawn(command)?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
let child = ForegroundChild::spawn(
|
||||||
|
command,
|
||||||
|
engine_state.is_interactive,
|
||||||
|
&engine_state.pipeline_externals_state,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Wrap the output into a `PipelineData::ByteStream`.
|
||||||
|
let child = nu_protocol::process::ChildProcess::new(child, None, false, call.head)?;
|
||||||
|
Ok(PipelineData::ByteStream(
|
||||||
|
ByteStream::child(child, call.head),
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
96
crates/nu-command/src/env/config/config_env.rs
vendored
96
crates/nu-command/src/env/config/config_env.rs
vendored
@ -1,7 +1,4 @@
|
|||||||
use nu_cmd_base::util::get_editor;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_engine::{command_prelude::*, env_to_strings};
|
|
||||||
use nu_protocol::{process::ChildProcess, ByteStream};
|
|
||||||
use nu_system::ForegroundChild;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigEnv;
|
pub struct ConfigEnv;
|
||||||
@ -15,7 +12,16 @@ impl Command for ConfigEnv {
|
|||||||
Signature::build(self.name())
|
Signature::build(self.name())
|
||||||
.category(Category::Env)
|
.category(Category::Env)
|
||||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||||
.switch("default", "Print default `env.nu` file instead.", Some('d'))
|
.switch(
|
||||||
|
"default",
|
||||||
|
"Print the internal default `env.nu` file instead.",
|
||||||
|
Some('d'),
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"doc",
|
||||||
|
"Print a commented `env.nu` with documentation instead.",
|
||||||
|
Some('s'),
|
||||||
|
)
|
||||||
// TODO: Signature narrower than what run actually supports theoretically
|
// TODO: Signature narrower than what run actually supports theoretically
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,18 +32,18 @@ impl Command for ConfigEnv {
|
|||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
description: "allow user to open and update nu env",
|
description: "open user's env.nu in the default editor",
|
||||||
example: "config env",
|
example: "config env",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "allow user to print default `env.nu` file",
|
description: "pretty-print a commented `env.nu` that explains common settings",
|
||||||
example: "config env --default,",
|
example: "config env --doc | nu-highlight,",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "allow saving the default `env.nu` locally",
|
description: "pretty-print the internal `env.nu` file which is loaded before the user's environment",
|
||||||
example: "config env --default | save -f ~/.config/nushell/default_env.nu",
|
example: "config env --default | nu-highlight,",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -50,66 +56,28 @@ impl Command for ConfigEnv {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let default_flag = call.has_flag(engine_state, stack, "default")?;
|
||||||
|
let doc_flag = call.has_flag(engine_state, stack, "doc")?;
|
||||||
|
if default_flag && doc_flag {
|
||||||
|
return Err(ShellError::IncompatibleParameters {
|
||||||
|
left_message: "can't use `--default` at the same time".into(),
|
||||||
|
left_span: call.get_flag_span(stack, "default").expect("has flag"),
|
||||||
|
right_message: "because of `--doc`".into(),
|
||||||
|
right_span: call.get_flag_span(stack, "doc").expect("has flag"),
|
||||||
|
});
|
||||||
|
}
|
||||||
// `--default` flag handling
|
// `--default` flag handling
|
||||||
if call.has_flag(engine_state, stack, "default")? {
|
if call.has_flag(engine_state, stack, "default")? {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data());
|
return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the editor executable.
|
// `--doc` flag handling
|
||||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
if doc_flag {
|
||||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
let head = call.head;
|
||||||
let cwd = engine_state.cwd(Some(stack))?;
|
return Ok(Value::string(nu_utils::get_doc_env(), head).into_pipeline_data());
|
||||||
let editor_executable = crate::which(&editor_name, &paths, cwd.as_ref()).ok_or(
|
}
|
||||||
ShellError::ExternalCommand {
|
|
||||||
label: format!("`{editor_name}` not found"),
|
|
||||||
help: "Failed to find the editor executable".into(),
|
|
||||||
span: call.head,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let Some(env_path) = engine_state.get_config_path("env-path") else {
|
super::config_::start_editor("env-path", engine_state, stack, call)
|
||||||
return Err(ShellError::GenericError {
|
|
||||||
error: "Could not find $nu.env-path".into(),
|
|
||||||
msg: "Could not find $nu.env-path".into(),
|
|
||||||
span: None,
|
|
||||||
help: None,
|
|
||||||
inner: vec![],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let env_path = env_path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
// Create the command.
|
|
||||||
let mut command = std::process::Command::new(editor_executable);
|
|
||||||
|
|
||||||
// Configure PWD.
|
|
||||||
command.current_dir(cwd);
|
|
||||||
|
|
||||||
// Configure environment variables.
|
|
||||||
let envs = env_to_strings(engine_state, stack)?;
|
|
||||||
command.env_clear();
|
|
||||||
command.envs(envs);
|
|
||||||
|
|
||||||
// Configure args.
|
|
||||||
command.arg(env_path);
|
|
||||||
command.args(editor_args);
|
|
||||||
|
|
||||||
// Spawn the child process. On Unix, also put the child process to
|
|
||||||
// foreground if we're in an interactive session.
|
|
||||||
#[cfg(windows)]
|
|
||||||
let child = ForegroundChild::spawn(command)?;
|
|
||||||
#[cfg(unix)]
|
|
||||||
let child = ForegroundChild::spawn(
|
|
||||||
command,
|
|
||||||
engine_state.is_interactive,
|
|
||||||
&engine_state.pipeline_externals_state,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Wrap the output into a `PipelineData::ByteStream`.
|
|
||||||
let child = ChildProcess::new(child, None, false, call.head)?;
|
|
||||||
Ok(PipelineData::ByteStream(
|
|
||||||
ByteStream::child(child, call.head),
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
195
crates/nu-command/src/env/config/config_flatten.rs
vendored
Normal file
195
crates/nu-command/src/env/config/config_flatten.rs
vendored
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_utils::JsonFlattener; // Ensure this import is present // Ensure this import is present
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ConfigFlatten;
|
||||||
|
|
||||||
|
impl Command for ConfigFlatten {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"config flatten"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build(self.name())
|
||||||
|
.category(Category::Debug)
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::record())])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Show the current configuration in a flattened form."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![Example {
|
||||||
|
description: "Show the current configuration in a flattened form",
|
||||||
|
example: "config flatten",
|
||||||
|
result: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
// Get the Config instance from the EngineState
|
||||||
|
let config = engine_state.get_config();
|
||||||
|
// Serialize the Config instance to JSON
|
||||||
|
let serialized_config =
|
||||||
|
serde_json::to_value(&**config).map_err(|err| ShellError::GenericError {
|
||||||
|
error: format!("Failed to serialize config to json: {err}"),
|
||||||
|
msg: "".into(),
|
||||||
|
span: Some(call.head),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
})?;
|
||||||
|
// Create a JsonFlattener instance with appropriate arguments
|
||||||
|
let flattener = JsonFlattener {
|
||||||
|
separator: ".",
|
||||||
|
alt_array_flattening: false,
|
||||||
|
preserve_arrays: true,
|
||||||
|
};
|
||||||
|
// Flatten the JSON value
|
||||||
|
let flattened_config_str = flattener.flatten(&serialized_config).to_string();
|
||||||
|
let flattened_values =
|
||||||
|
convert_string_to_value(&flattened_config_str, engine_state, call.head)?;
|
||||||
|
|
||||||
|
Ok(flattened_values.into_pipeline_data())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From here below is taken from `from json`. Would be nice to have a nu-utils-value crate that could be shared
|
||||||
|
fn convert_string_to_value(
|
||||||
|
string_input: &str,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
span: Span,
|
||||||
|
) -> Result<Value, ShellError> {
|
||||||
|
match nu_json::from_str(string_input) {
|
||||||
|
Ok(value) => Ok(convert_nujson_to_value(None, value, engine_state, span)),
|
||||||
|
|
||||||
|
Err(x) => match x {
|
||||||
|
nu_json::Error::Syntax(_, row, col) => {
|
||||||
|
let label = x.to_string();
|
||||||
|
let label_span = convert_row_column_to_span(row, col, string_input);
|
||||||
|
Err(ShellError::GenericError {
|
||||||
|
error: "Error while parsing JSON text".into(),
|
||||||
|
msg: "error parsing JSON text".into(),
|
||||||
|
span: Some(span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![ShellError::OutsideSpannedLabeledError {
|
||||||
|
src: string_input.into(),
|
||||||
|
error: "Error while parsing JSON text".into(),
|
||||||
|
msg: label,
|
||||||
|
span: label_span,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
x => Err(ShellError::CantConvert {
|
||||||
|
to_type: format!("structured json data ({x})"),
|
||||||
|
from_type: "string".into(),
|
||||||
|
span,
|
||||||
|
help: None,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_nujson_to_value(
|
||||||
|
key: Option<String>,
|
||||||
|
value: nu_json::Value,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
span: Span,
|
||||||
|
) -> Value {
|
||||||
|
match value {
|
||||||
|
nu_json::Value::Array(array) => Value::list(
|
||||||
|
array
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| convert_nujson_to_value(key.clone(), x, engine_state, span))
|
||||||
|
.collect(),
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
nu_json::Value::Bool(b) => Value::bool(b, span),
|
||||||
|
nu_json::Value::F64(f) => Value::float(f, span),
|
||||||
|
nu_json::Value::I64(i) => {
|
||||||
|
if let Some(closure_str) = expand_closure(key.clone(), i, engine_state) {
|
||||||
|
Value::string(closure_str, span)
|
||||||
|
} else {
|
||||||
|
Value::int(i, span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nu_json::Value::Null => Value::nothing(span),
|
||||||
|
nu_json::Value::Object(k) => Value::record(
|
||||||
|
k.into_iter()
|
||||||
|
.map(|(k, v)| {
|
||||||
|
let mut key = k.clone();
|
||||||
|
// Keep .Closure.val and .block_id as part of the key during conversion to value
|
||||||
|
let value = convert_nujson_to_value(Some(key.clone()), v, engine_state, span);
|
||||||
|
// Replace .Closure.val and .block_id from the key after the conversion
|
||||||
|
if key.contains(".Closure.val") || key.contains(".block_id") {
|
||||||
|
key = key.replace(".Closure.val", "").replace(".block_id", "");
|
||||||
|
}
|
||||||
|
(key, value)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
nu_json::Value::U64(u) => {
|
||||||
|
if u > i64::MAX as u64 {
|
||||||
|
Value::error(
|
||||||
|
ShellError::CantConvert {
|
||||||
|
to_type: "i64 sized integer".into(),
|
||||||
|
from_type: "value larger than i64".into(),
|
||||||
|
span,
|
||||||
|
help: None,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
} else if let Some(closure_str) = expand_closure(key.clone(), u as i64, engine_state) {
|
||||||
|
Value::string(closure_str, span)
|
||||||
|
} else {
|
||||||
|
Value::int(u as i64, span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nu_json::Value::String(s) => Value::string(s, span),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the block_id is a real block id, then it should expand into the closure contents, otherwise return None
|
||||||
|
fn expand_closure(
|
||||||
|
key: Option<String>,
|
||||||
|
block_id: i64,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
) -> Option<String> {
|
||||||
|
match key {
|
||||||
|
Some(key) if key.contains(".Closure.val") || key.contains(".block_id") => engine_state
|
||||||
|
.try_get_block(nu_protocol::BlockId::new(block_id as usize))
|
||||||
|
.and_then(|block| block.span)
|
||||||
|
.map(|span| {
|
||||||
|
let contents = engine_state.get_span_contents(span);
|
||||||
|
String::from_utf8_lossy(contents).to_string()
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts row+column to a Span, assuming bytes (1-based rows)
|
||||||
|
fn convert_row_column_to_span(row: usize, col: usize, contents: &str) -> Span {
|
||||||
|
let mut cur_row = 1;
|
||||||
|
let mut cur_col = 1;
|
||||||
|
|
||||||
|
for (offset, curr_byte) in contents.bytes().enumerate() {
|
||||||
|
if curr_byte == b'\n' {
|
||||||
|
cur_row += 1;
|
||||||
|
cur_col = 1;
|
||||||
|
}
|
||||||
|
if cur_row >= row && cur_col >= col {
|
||||||
|
return Span::new(offset, offset);
|
||||||
|
} else {
|
||||||
|
cur_col += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Span::new(contents.len(), contents.len())
|
||||||
|
}
|
97
crates/nu-command/src/env/config/config_nu.rs
vendored
97
crates/nu-command/src/env/config/config_nu.rs
vendored
@ -1,7 +1,4 @@
|
|||||||
use nu_cmd_base::util::get_editor;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_engine::{command_prelude::*, env_to_strings};
|
|
||||||
use nu_protocol::{process::ChildProcess, ByteStream};
|
|
||||||
use nu_system::ForegroundChild;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigNu;
|
pub struct ConfigNu;
|
||||||
@ -17,10 +14,14 @@ impl Command for ConfigNu {
|
|||||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||||
.switch(
|
.switch(
|
||||||
"default",
|
"default",
|
||||||
"Print default `config.nu` file instead.",
|
"Print the internal default `config.nu` file instead.",
|
||||||
Some('d'),
|
Some('d'),
|
||||||
)
|
)
|
||||||
// TODO: Signature narrower than what run actually supports theoretically
|
.switch(
|
||||||
|
"doc",
|
||||||
|
"Print a commented `config.nu` with documentation instead.",
|
||||||
|
Some('s'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
@ -30,18 +31,19 @@ impl Command for ConfigNu {
|
|||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
description: "allow user to open and update nu config",
|
description: "open user's config.nu in the default editor",
|
||||||
example: "config nu",
|
example: "config nu",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "allow user to print default `config.nu` file",
|
description: "pretty-print a commented `config.nu` that explains common settings",
|
||||||
example: "config nu --default,",
|
example: "config nu --doc | nu-highlight",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "allow saving the default `config.nu` locally",
|
description:
|
||||||
example: "config nu --default | save -f ~/.config/nushell/default_config.nu",
|
"pretty-print the internal `config.nu` file which is loaded before user's config",
|
||||||
|
example: "config nu --default | nu-highlight",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -54,66 +56,29 @@ impl Command for ConfigNu {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let default_flag = call.has_flag(engine_state, stack, "default")?;
|
||||||
|
let doc_flag = call.has_flag(engine_state, stack, "doc")?;
|
||||||
|
if default_flag && doc_flag {
|
||||||
|
return Err(ShellError::IncompatibleParameters {
|
||||||
|
left_message: "can't use `--default` at the same time".into(),
|
||||||
|
left_span: call.get_flag_span(stack, "default").expect("has flag"),
|
||||||
|
right_message: "because of `--doc`".into(),
|
||||||
|
right_span: call.get_flag_span(stack, "doc").expect("has flag"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// `--default` flag handling
|
// `--default` flag handling
|
||||||
if call.has_flag(engine_state, stack, "default")? {
|
if default_flag {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data());
|
return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the editor executable.
|
// `--doc` flag handling
|
||||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
if doc_flag {
|
||||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
let head = call.head;
|
||||||
let cwd = engine_state.cwd(Some(stack))?;
|
return Ok(Value::string(nu_utils::get_doc_config(), head).into_pipeline_data());
|
||||||
let editor_executable = crate::which(&editor_name, &paths, cwd.as_ref()).ok_or(
|
}
|
||||||
ShellError::ExternalCommand {
|
|
||||||
label: format!("`{editor_name}` not found"),
|
|
||||||
help: "Failed to find the editor executable".into(),
|
|
||||||
span: call.head,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let Some(config_path) = engine_state.get_config_path("config-path") else {
|
super::config_::start_editor("config-path", engine_state, stack, call)
|
||||||
return Err(ShellError::GenericError {
|
|
||||||
error: "Could not find $nu.config-path".into(),
|
|
||||||
msg: "Could not find $nu.config-path".into(),
|
|
||||||
span: None,
|
|
||||||
help: None,
|
|
||||||
inner: vec![],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let config_path = config_path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
// Create the command.
|
|
||||||
let mut command = std::process::Command::new(editor_executable);
|
|
||||||
|
|
||||||
// Configure PWD.
|
|
||||||
command.current_dir(cwd);
|
|
||||||
|
|
||||||
// Configure environment variables.
|
|
||||||
let envs = env_to_strings(engine_state, stack)?;
|
|
||||||
command.env_clear();
|
|
||||||
command.envs(envs);
|
|
||||||
|
|
||||||
// Configure args.
|
|
||||||
command.arg(config_path);
|
|
||||||
command.args(editor_args);
|
|
||||||
|
|
||||||
// Spawn the child process. On Unix, also put the child process to
|
|
||||||
// foreground if we're in an interactive session.
|
|
||||||
#[cfg(windows)]
|
|
||||||
let child = ForegroundChild::spawn(command)?;
|
|
||||||
#[cfg(unix)]
|
|
||||||
let child = ForegroundChild::spawn(
|
|
||||||
command,
|
|
||||||
engine_state.is_interactive,
|
|
||||||
&engine_state.pipeline_externals_state,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Wrap the output into a `PipelineData::ByteStream`.
|
|
||||||
let child = ChildProcess::new(child, None, false, call.head)?;
|
|
||||||
Ok(PipelineData::ByteStream(
|
|
||||||
ByteStream::child(child, call.head),
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
crates/nu-command/src/env/config/mod.rs
vendored
3
crates/nu-command/src/env/config/mod.rs
vendored
@ -1,8 +1,11 @@
|
|||||||
mod config_;
|
mod config_;
|
||||||
mod config_env;
|
mod config_env;
|
||||||
|
mod config_flatten;
|
||||||
mod config_nu;
|
mod config_nu;
|
||||||
mod config_reset;
|
mod config_reset;
|
||||||
|
|
||||||
pub use config_::ConfigMeta;
|
pub use config_::ConfigMeta;
|
||||||
pub use config_env::ConfigEnv;
|
pub use config_env::ConfigEnv;
|
||||||
|
pub use config_flatten::ConfigFlatten;
|
||||||
pub use config_nu::ConfigNu;
|
pub use config_nu::ConfigNu;
|
||||||
pub use config_reset::ConfigReset;
|
pub use config_reset::ConfigReset;
|
||||||
|
1
crates/nu-command/src/env/mod.rs
vendored
1
crates/nu-command/src/env/mod.rs
vendored
@ -5,6 +5,7 @@ mod source_env;
|
|||||||
mod with_env;
|
mod with_env;
|
||||||
|
|
||||||
pub use config::ConfigEnv;
|
pub use config::ConfigEnv;
|
||||||
|
pub use config::ConfigFlatten;
|
||||||
pub use config::ConfigMeta;
|
pub use config::ConfigMeta;
|
||||||
pub use config::ConfigNu;
|
pub use config::ConfigNu;
|
||||||
pub use config::ConfigReset;
|
pub use config::ConfigReset;
|
||||||
|
@ -103,3 +103,9 @@ fn is_root_impl() -> bool {
|
|||||||
|
|
||||||
elevated
|
elevated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn is_root_impl() -> bool {
|
||||||
|
// in wasm we don't have a user system, so technically we are never root
|
||||||
|
false
|
||||||
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use super::util::get_rest_for_glob_pattern;
|
|
||||||
use crate::{DirBuilder, DirInfo, FileInfo};
|
use crate::{DirBuilder, DirInfo, FileInfo};
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use nu_engine::{command_prelude::*, current_dir};
|
use nu_engine::{command_prelude::*, current_dir};
|
||||||
@ -13,8 +12,8 @@ pub struct Du;
|
|||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
pub struct DuArgs {
|
pub struct DuArgs {
|
||||||
path: Option<Spanned<NuGlob>>,
|
path: Option<Spanned<NuGlob>>,
|
||||||
all: bool,
|
|
||||||
deref: bool,
|
deref: bool,
|
||||||
|
long: bool,
|
||||||
exclude: Option<Spanned<NuGlob>>,
|
exclude: Option<Spanned<NuGlob>>,
|
||||||
#[serde(rename = "max-depth")]
|
#[serde(rename = "max-depth")]
|
||||||
max_depth: Option<Spanned<i64>>,
|
max_depth: Option<Spanned<i64>>,
|
||||||
@ -40,16 +39,16 @@ impl Command for Du {
|
|||||||
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
|
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
|
||||||
"Starting directory.",
|
"Starting directory.",
|
||||||
)
|
)
|
||||||
.switch(
|
|
||||||
"all",
|
|
||||||
"Output file sizes as well as directory sizes",
|
|
||||||
Some('a'),
|
|
||||||
)
|
|
||||||
.switch(
|
.switch(
|
||||||
"deref",
|
"deref",
|
||||||
"Dereference symlinks to their targets for size",
|
"Dereference symlinks to their targets for size",
|
||||||
Some('r'),
|
Some('r'),
|
||||||
)
|
)
|
||||||
|
.switch(
|
||||||
|
"long",
|
||||||
|
"Get underlying directories and files for each entry",
|
||||||
|
Some('l'),
|
||||||
|
)
|
||||||
.named(
|
.named(
|
||||||
"exclude",
|
"exclude",
|
||||||
SyntaxShape::GlobPattern,
|
SyntaxShape::GlobPattern,
|
||||||
@ -95,13 +94,13 @@ impl Command for Du {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let all = call.has_flag(engine_state, stack, "all")?;
|
|
||||||
let deref = call.has_flag(engine_state, stack, "deref")?;
|
let deref = call.has_flag(engine_state, stack, "deref")?;
|
||||||
|
let long = call.has_flag(engine_state, stack, "long")?;
|
||||||
let exclude = call.get_flag(engine_state, stack, "exclude")?;
|
let exclude = call.get_flag(engine_state, stack, "exclude")?;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let current_dir = current_dir(engine_state, stack)?;
|
let current_dir = current_dir(engine_state, stack)?;
|
||||||
|
|
||||||
let paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
let paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
|
||||||
let paths = if !call.has_positional_args(stack, 0) {
|
let paths = if !call.has_positional_args(stack, 0) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@ -112,8 +111,8 @@ impl Command for Du {
|
|||||||
None => {
|
None => {
|
||||||
let args = DuArgs {
|
let args = DuArgs {
|
||||||
path: None,
|
path: None,
|
||||||
all,
|
|
||||||
deref,
|
deref,
|
||||||
|
long,
|
||||||
exclude,
|
exclude,
|
||||||
max_depth,
|
max_depth,
|
||||||
min_size,
|
min_size,
|
||||||
@ -128,8 +127,8 @@ impl Command for Du {
|
|||||||
for p in paths {
|
for p in paths {
|
||||||
let args = DuArgs {
|
let args = DuArgs {
|
||||||
path: Some(p),
|
path: Some(p),
|
||||||
all,
|
|
||||||
deref,
|
deref,
|
||||||
|
long,
|
||||||
exclude: exclude.clone(),
|
exclude: exclude.clone(),
|
||||||
max_depth,
|
max_depth,
|
||||||
min_size,
|
min_size,
|
||||||
@ -175,7 +174,6 @@ fn du_for_one_pattern(
|
|||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let include_files = args.all;
|
|
||||||
let mut paths = match args.path {
|
let mut paths = match args.path {
|
||||||
Some(p) => nu_engine::glob_from(&p, current_dir, span, None),
|
Some(p) => nu_engine::glob_from(&p, current_dir, span, None),
|
||||||
// The * pattern should never fail.
|
// The * pattern should never fail.
|
||||||
@ -189,17 +187,10 @@ fn du_for_one_pattern(
|
|||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
.map(|f| f.1)?
|
.map(|f| f.1)?;
|
||||||
.filter(move |p| {
|
|
||||||
if include_files {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
matches!(p, Ok(f) if f.is_dir())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let all = args.all;
|
|
||||||
let deref = args.deref;
|
let deref = args.deref;
|
||||||
|
let long = args.long;
|
||||||
let max_depth = args.max_depth.map(|f| f.item as u64);
|
let max_depth = args.max_depth.map(|f| f.item as u64);
|
||||||
let min_size = args.min_size.map(|f| f.item as u64);
|
let min_size = args.min_size.map(|f| f.item as u64);
|
||||||
|
|
||||||
@ -208,7 +199,7 @@ fn du_for_one_pattern(
|
|||||||
min: min_size,
|
min: min_size,
|
||||||
deref,
|
deref,
|
||||||
exclude,
|
exclude,
|
||||||
all,
|
long,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut output: Vec<Value> = vec![];
|
let mut output: Vec<Value> = vec![];
|
||||||
@ -217,7 +208,7 @@ fn du_for_one_pattern(
|
|||||||
Ok(a) => {
|
Ok(a) => {
|
||||||
if a.is_dir() {
|
if a.is_dir() {
|
||||||
output.push(DirInfo::new(a, ¶ms, max_depth, span, signals)?.into());
|
output.push(DirInfo::new(a, ¶ms, max_depth, span, signals)?.into());
|
||||||
} else if let Ok(v) = FileInfo::new(a, deref, span) {
|
} else if let Ok(v) = FileInfo::new(a, deref, span, params.long) {
|
||||||
output.push(v.into());
|
output.push(v.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::Signals;
|
use nu_protocol::{ListStream, Signals};
|
||||||
use wax::{Glob as WaxGlob, WalkBehavior, WalkEntry};
|
use wax::{Glob as WaxGlob, WalkBehavior, WalkEntry};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -223,6 +223,7 @@ impl Command for Glob {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.into_owned()
|
||||||
.not(np)
|
.not(np)
|
||||||
.map_err(|err| ShellError::GenericError {
|
.map_err(|err| ShellError::GenericError {
|
||||||
error: "error with glob's not pattern".into(),
|
error: "error with glob's not pattern".into(),
|
||||||
@ -249,6 +250,7 @@ impl Command for Glob {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.into_owned()
|
||||||
.flatten();
|
.flatten();
|
||||||
glob_to_value(
|
glob_to_value(
|
||||||
engine_state.signals(),
|
engine_state.signals(),
|
||||||
@ -258,11 +260,9 @@ impl Command for Glob {
|
|||||||
no_symlinks,
|
no_symlinks,
|
||||||
span,
|
span,
|
||||||
)
|
)
|
||||||
}?;
|
};
|
||||||
|
|
||||||
Ok(result
|
Ok(result.into_pipeline_data(span, engine_state.signals().clone()))
|
||||||
.into_iter()
|
|
||||||
.into_pipeline_data(span, engine_state.signals().clone()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,29 +281,33 @@ fn convert_patterns(columns: &[Value]) -> Result<Vec<String>, ShellError> {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn glob_to_value<'a>(
|
fn glob_to_value(
|
||||||
signals: &Signals,
|
signals: &Signals,
|
||||||
glob_results: impl Iterator<Item = WalkEntry<'a>>,
|
glob_results: impl Iterator<Item = WalkEntry<'static>> + Send + 'static,
|
||||||
no_dirs: bool,
|
no_dirs: bool,
|
||||||
no_files: bool,
|
no_files: bool,
|
||||||
no_symlinks: bool,
|
no_symlinks: bool,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> Result<Vec<Value>, ShellError> {
|
) -> ListStream {
|
||||||
let mut result: Vec<Value> = Vec::new();
|
let map_signals = signals.clone();
|
||||||
for entry in glob_results {
|
let result = glob_results.filter_map(move |entry| {
|
||||||
signals.check(span)?;
|
if let Err(err) = map_signals.check(span) {
|
||||||
|
return Some(Value::error(err, span));
|
||||||
|
};
|
||||||
let file_type = entry.file_type();
|
let file_type = entry.file_type();
|
||||||
|
|
||||||
if !(no_dirs && file_type.is_dir()
|
if !(no_dirs && file_type.is_dir()
|
||||||
|| no_files && file_type.is_file()
|
|| no_files && file_type.is_file()
|
||||||
|| no_symlinks && file_type.is_symlink())
|
|| no_symlinks && file_type.is_symlink())
|
||||||
{
|
{
|
||||||
result.push(Value::string(
|
Some(Value::string(
|
||||||
entry.into_path().to_string_lossy().to_string(),
|
entry.into_path().to_string_lossy().to_string(),
|
||||||
span,
|
span,
|
||||||
));
|
))
|
||||||
}
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(result)
|
ListStream::new(result, span, signals.clone())
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use super::util::get_rest_for_glob_pattern;
|
|
||||||
use crate::{DirBuilder, DirInfo};
|
use crate::{DirBuilder, DirInfo};
|
||||||
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
|
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
|
||||||
use nu_engine::glob_from;
|
use nu_engine::glob_from;
|
||||||
@ -9,13 +8,12 @@ use nu_path::{expand_path_with, expand_to_real_path};
|
|||||||
use nu_protocol::{DataSource, NuGlob, PipelineMetadata, Signals};
|
use nu_protocol::{DataSource, NuGlob, PipelineMetadata, Signals};
|
||||||
use pathdiff::diff_paths;
|
use pathdiff::diff_paths;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::mpsc,
|
sync::{mpsc, Arc, Mutex},
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,7 +112,7 @@ impl Command for Ls {
|
|||||||
call_span,
|
call_span,
|
||||||
};
|
};
|
||||||
|
|
||||||
let pattern_arg = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
let pattern_arg = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
|
||||||
let input_pattern_arg = if !call.has_positional_args(stack, 0) {
|
let input_pattern_arg = if !call.has_positional_args(stack, 0) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@ -287,28 +285,10 @@ fn ls_for_one_pattern(
|
|||||||
nu_path::expand_path_with(pat.item.as_ref(), &cwd, pat.item.is_expand());
|
nu_path::expand_path_with(pat.item.as_ref(), &cwd, pat.item.is_expand());
|
||||||
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
|
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
|
||||||
if !directory && tmp_expanded.is_dir() {
|
if !directory && tmp_expanded.is_dir() {
|
||||||
if permission_denied(&tmp_expanded) {
|
if read_dir(&tmp_expanded, p_tag, use_threads)?
|
||||||
#[cfg(unix)]
|
.next()
|
||||||
let error_msg = format!(
|
.is_none()
|
||||||
"The permissions of {:o} do not allow access for this user",
|
{
|
||||||
tmp_expanded
|
|
||||||
.metadata()
|
|
||||||
.expect("this shouldn't be called since we already know there is a dir")
|
|
||||||
.permissions()
|
|
||||||
.mode()
|
|
||||||
& 0o0777
|
|
||||||
);
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
let error_msg = String::from("Permission denied");
|
|
||||||
return Err(ShellError::GenericError {
|
|
||||||
error: "Permission denied".into(),
|
|
||||||
msg: error_msg,
|
|
||||||
span: Some(p_tag),
|
|
||||||
help: None,
|
|
||||||
inner: vec![],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if is_empty_dir(&tmp_expanded) {
|
|
||||||
return Ok(Value::test_nothing().into_pipeline_data());
|
return Ok(Value::test_nothing().into_pipeline_data());
|
||||||
}
|
}
|
||||||
just_read_dir = !(pat.item.is_expand() && pat.item.as_ref().contains(GLOB_CHARS));
|
just_read_dir = !(pat.item.is_expand() && pat.item.as_ref().contains(GLOB_CHARS));
|
||||||
@ -327,7 +307,7 @@ fn ls_for_one_pattern(
|
|||||||
// Avoid pushing "*" to the default path when directory (do not show contents) flag is true
|
// Avoid pushing "*" to the default path when directory (do not show contents) flag is true
|
||||||
if directory {
|
if directory {
|
||||||
(NuGlob::Expand(".".to_string()), false)
|
(NuGlob::Expand(".".to_string()), false)
|
||||||
} else if is_empty_dir(&cwd) {
|
} else if read_dir(&cwd, p_tag, use_threads)?.next().is_none() {
|
||||||
return Ok(Value::test_nothing().into_pipeline_data());
|
return Ok(Value::test_nothing().into_pipeline_data());
|
||||||
} else {
|
} else {
|
||||||
(NuGlob::Expand("*".to_string()), false)
|
(NuGlob::Expand("*".to_string()), false)
|
||||||
@ -339,7 +319,7 @@ fn ls_for_one_pattern(
|
|||||||
let path = pattern_arg.into_spanned(p_tag);
|
let path = pattern_arg.into_spanned(p_tag);
|
||||||
let (prefix, paths) = if just_read_dir {
|
let (prefix, paths) = if just_read_dir {
|
||||||
let expanded = nu_path::expand_path_with(path.item.as_ref(), &cwd, path.item.is_expand());
|
let expanded = nu_path::expand_path_with(path.item.as_ref(), &cwd, path.item.is_expand());
|
||||||
let paths = read_dir(&expanded)?;
|
let paths = read_dir(&expanded, p_tag, use_threads)?;
|
||||||
// just need to read the directory, so prefix is path itself.
|
// just need to read the directory, so prefix is path itself.
|
||||||
(Some(expanded), paths)
|
(Some(expanded), paths)
|
||||||
} else {
|
} else {
|
||||||
@ -492,20 +472,6 @@ fn ls_for_one_pattern(
|
|||||||
.into_pipeline_data(call_span, signals.clone()))
|
.into_pipeline_data(call_span, signals.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn permission_denied(dir: impl AsRef<Path>) -> bool {
|
|
||||||
match dir.as_ref().read_dir() {
|
|
||||||
Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
|
|
||||||
Ok(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_empty_dir(dir: impl AsRef<Path>) -> bool {
|
|
||||||
match dir.as_ref().read_dir() {
|
|
||||||
Err(_) => true,
|
|
||||||
Ok(mut s) => s.next().is_none(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
|
fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
@ -979,10 +945,37 @@ mod windows_helper {
|
|||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn read_dir(
|
fn read_dir(
|
||||||
f: &Path,
|
f: &Path,
|
||||||
|
span: Span,
|
||||||
|
use_threads: bool,
|
||||||
) -> Result<Box<dyn Iterator<Item = Result<PathBuf, ShellError>> + Send>, ShellError> {
|
) -> Result<Box<dyn Iterator<Item = Result<PathBuf, ShellError>> + Send>, ShellError> {
|
||||||
let iter = f.read_dir()?.map(|d| {
|
let items = f
|
||||||
|
.read_dir()
|
||||||
|
.map_err(|error| {
|
||||||
|
if error.kind() == std::io::ErrorKind::PermissionDenied {
|
||||||
|
return ShellError::GenericError {
|
||||||
|
error: "Permission denied".into(),
|
||||||
|
msg: "The permissions may not allow access for this user".into(),
|
||||||
|
span: Some(span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
error.into()
|
||||||
|
})?
|
||||||
|
.map(|d| {
|
||||||
d.map(|r| r.path())
|
d.map(|r| r.path())
|
||||||
.map_err(|e| ShellError::IOError { msg: e.to_string() })
|
.map_err(|e| ShellError::IOError { msg: e.to_string() })
|
||||||
});
|
});
|
||||||
Ok(Box::new(iter))
|
if !use_threads {
|
||||||
|
let mut collected = items.collect::<Vec<_>>();
|
||||||
|
collected.sort_by(|a, b| match (a, b) {
|
||||||
|
(Ok(a), Ok(b)) => a.cmp(b),
|
||||||
|
(Ok(_), Err(_)) => Ordering::Greater,
|
||||||
|
(Err(_), Ok(_)) => Ordering::Less,
|
||||||
|
(Err(_), Err(_)) => Ordering::Equal,
|
||||||
|
});
|
||||||
|
return Ok(Box::new(collected.into_iter()));
|
||||||
|
}
|
||||||
|
Ok(Box::new(items))
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ mod ucp;
|
|||||||
mod umkdir;
|
mod umkdir;
|
||||||
mod umv;
|
mod umv;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod utouch;
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
pub use self::open::Open;
|
pub use self::open::Open;
|
||||||
@ -27,4 +28,5 @@ pub use touch::Touch;
|
|||||||
pub use ucp::UCp;
|
pub use ucp::UCp;
|
||||||
pub use umkdir::UMkdir;
|
pub use umkdir::UMkdir;
|
||||||
pub use umv::UMv;
|
pub use umv::UMv;
|
||||||
|
pub use utouch::UTouch;
|
||||||
pub use watch::Watch;
|
pub use watch::Watch;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use super::util::get_rest_for_glob_pattern;
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use nu_engine::{command_prelude::*, current_dir, get_eval_block};
|
use nu_engine::{command_prelude::*, current_dir, get_eval_block};
|
||||||
use nu_protocol::{ast, ByteStream, DataSource, NuGlob, PipelineMetadata};
|
use nu_protocol::{ast, DataSource, NuGlob, PipelineMetadata};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
@ -53,7 +52,7 @@ impl Command for Open {
|
|||||||
let call_span = call.head;
|
let call_span = call.head;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
|
||||||
let eval_block = get_eval_block(engine_state);
|
let eval_block = get_eval_block(engine_state);
|
||||||
|
|
||||||
if paths.is_empty() && !call.has_positional_args(stack, 0) {
|
if paths.is_empty() && !call.has_positional_args(stack, 0) {
|
||||||
@ -146,6 +145,9 @@ impl Command for Open {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Assigning content type should only happen in raw mode. Otherwise, the content
|
||||||
|
// will potentially be in one of the built-in nushell `from xxx` formats and therefore
|
||||||
|
// cease to be in the original content-type.... or so I'm told. :)
|
||||||
let content_type = if raw {
|
let content_type = if raw {
|
||||||
path.extension()
|
path.extension()
|
||||||
.map(|ext| ext.to_string_lossy().to_string())
|
.map(|ext| ext.to_string_lossy().to_string())
|
||||||
@ -283,6 +285,9 @@ fn detect_content_type(extension: &str) -> Option<String> {
|
|||||||
match extension {
|
match extension {
|
||||||
// Per RFC-9512, application/yaml should be used
|
// Per RFC-9512, application/yaml should be used
|
||||||
"yaml" | "yml" => Some("application/yaml".to_string()),
|
"yaml" | "yml" => Some("application/yaml".to_string()),
|
||||||
|
"nu" => Some("application/x-nuscript".to_string()),
|
||||||
|
"json" | "jsonl" | "ndjson" => Some("application/json".to_string()),
|
||||||
|
"nuon" => Some("application/x-nuon".to_string()),
|
||||||
_ => mime_guess::from_ext(extension)
|
_ => mime_guess::from_ext(extension)
|
||||||
.first()
|
.first()
|
||||||
.map(|mime| mime.to_string()),
|
.map(|mime| mime.to_string()),
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user