mirror of
https://github.com/nushell/nushell.git
synced 2025-07-01 07:00:37 +02:00
Compare commits
425 Commits
back-to-th
...
0.102.0
Author | SHA1 | Date | |
---|---|---|---|
1aa2ed1947 | |||
f04db2a7a3 | |||
30b3c42b37 | |||
4424481487 | |||
8b431e3a2e | |||
c58b432c21 | |||
34c09d8b35 | |||
13d5a15f75 | |||
339c5b7c83 | |||
5291f978c2 | |||
63fa6a6df7 | |||
4540f3829e | |||
1349187e17 | |||
b55ed69c92 | |||
948965d42f | |||
f4205132c7 | |||
5eae08ac76 | |||
3836da0cf1 | |||
6be42d94d9 | |||
945e9511ce | |||
cce12efe48 | |||
aa62de78e6 | |||
08b5d5cce5 | |||
03bb144150 | |||
5fa79e6e5f | |||
080b501ba8 | |||
66bc0542e0 | |||
ec1f7deb23 | |||
45f9d03025 | |||
4bc28f1752 | |||
a2705f9eb5 | |||
c0b4d19761 | |||
b53271b86a | |||
5ca4e903c8 | |||
0ad5f4389c | |||
7ea4895513 | |||
f46f8b286b | |||
f88ed6ecd5 | |||
a011791631 | |||
c783b07d58 | |||
e3e2554b3d | |||
926b0407c5 | |||
22a01d7e76 | |||
299453ecb7 | |||
fd684a204c | |||
cdbb3ee7b9 | |||
f0f6b3a3e5 | |||
93e121782c | |||
befeddad59 | |||
73c08fcb2b | |||
9a0ae7c4c0 | |||
28ca0e7116 | |||
84c720daf5 | |||
cdb082e92d | |||
0666b3784f | |||
379d89369c | |||
2bd345c367 | |||
0e418688d4 | |||
b97d89adb6 | |||
ee84435a0e | |||
500cd35ad2 | |||
3f5ebd75b6 | |||
75105033b2 | |||
8759936636 | |||
4dcaf2a201 | |||
089c5221cc | |||
0587308684 | |||
6eff420e17 | |||
d66f8cca40 | |||
06938659d2 | |||
46566296c0 | |||
4e1b06cb51 | |||
b99a8c9d80 | |||
b34547334a | |||
d9bfcb4c09 | |||
8ce14a7c86 | |||
301d1370c4 | |||
306e305b65 | |||
e117706518 | |||
737ea3940e | |||
e5337b50a9 | |||
23dc1b600a | |||
f05162811c | |||
0b71eb201c | |||
707ab1df6a | |||
c811d86dbd | |||
902e6d7a27 | |||
827e31191d | |||
b9b3101bd9 | |||
8e8a60a432 | |||
72d50cf8b7 | |||
3a1601de8e | |||
3f8dd1b705 | |||
f360489f1e | |||
79f19f2fc7 | |||
5cf6dea997 | |||
214714e0ab | |||
d894c8befe | |||
cc4d4acc6b | |||
dc52a6fec5 | |||
16e174be7e | |||
8e41a308cd | |||
787f292ca7 | |||
dad956b2ee | |||
1f477c8eb1 | |||
6260fa9f07 | |||
88f44701a9 | |||
9ed944312f | |||
6eb14522b6 | |||
ac12b02437 | |||
9ed2ca792f | |||
ebabca575c | |||
b60f91f722 | |||
2b4c54d383 | |||
ed1381adc4 | |||
1b7fabd1fd | |||
87a562e24b | |||
b5ff46db6a | |||
8b086d3613 | |||
d702c4605a | |||
6325bc5e54 | |||
25d90fa603 | |||
86f7f53f85 | |||
461eb43d9d | |||
df3892f323 | |||
0d3f76ddef | |||
816b9a6953 | |||
80788636ee | |||
c46ca36bcd | |||
62bd6fe08b | |||
f69b22f00b | |||
c6523eb8d9 | |||
76afa74320 | |||
a0d4ae18ee | |||
4884894ddb | |||
e7877db078 | |||
1181349c22 | |||
378395c22c | |||
2bcf2389aa | |||
a65e5ab01d | |||
4ff4e3f93d | |||
d36514a323 | |||
4401924128 | |||
5314b31b12 | |||
b2b5b89a92 | |||
76bbd41e43 | |||
5f3c8d45d8 | |||
38694a9850 | |||
0a0475ebad | |||
38ffcaad7b | |||
1b01598840 | |||
45ff964cbd | |||
81baf53814 | |||
6ebc0fc3ff | |||
b1da50774a | |||
469e23cae4 | |||
23ba613b00 | |||
f2dcae570c | |||
f1ce0c98fd | |||
35d2750757 | |||
4b1f4e63c3 | |||
c29bcc94e7 | |||
d3cbcf401f | |||
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 |
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
@ -96,7 +96,7 @@ jobs:
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||
|
||||
- name: Install Nushell
|
||||
run: cargo install --path . --locked --no-default-features
|
||||
run: cargo install --path . --locked --no-default-features --force
|
||||
|
||||
- name: Standard library tests
|
||||
run: nu -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std'
|
||||
@ -162,3 +162,50 @@ jobs:
|
||||
else
|
||||
echo "no changes in working directory";
|
||||
fi
|
||||
|
||||
wasm:
|
||||
env:
|
||||
WASM_OPTIONS: --no-default-features --target wasm32-unknown-unknown
|
||||
CLIPPY_CONF_DIR: ${{ github.workspace }}/clippy/wasm/
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
job:
|
||||
- name: Build WASM
|
||||
command: cargo build
|
||||
args:
|
||||
- name: Clippy WASM
|
||||
command: cargo clippy
|
||||
args: -- $CLIPPY_OPTIONS
|
||||
|
||||
name: ${{ matrix.job.name }}
|
||||
|
||||
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: ${{ matrix.job.command }} -p nu-cmd-base $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-cmd-extra $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-cmd-lang $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-color-config $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-command $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-derive-value $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-engine $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-glob $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-json $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-parser $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-path $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-pretty-hex $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-protocol $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-std $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-system $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-table $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-term-grid $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nu-utils $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
- run: ${{ matrix.job.command }} -p nuon $WASM_OPTIONS ${{ matrix.job.args }}
|
||||
|
18
.github/workflows/milestone.yml
vendored
18
.github/workflows/milestone.yml
vendored
@ -1,8 +1,11 @@
|
||||
# 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
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
@ -10,9 +13,18 @@ jobs:
|
||||
update-milestone:
|
||||
runs-on: ubuntu-latest
|
||||
name: Milestone Update
|
||||
if: ${{github.event.pull_request.merged == true}}
|
||||
steps:
|
||||
- name: Set Milestone
|
||||
- name: Set Milestone for PR
|
||||
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@v2
|
||||
if: github.event.issue.state == 'closed'
|
||||
with:
|
||||
action: bind-issue
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
10
.github/workflows/nightly-build.yml
vendored
10
.github/workflows/nightly-build.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
uses: hustcer/setup-nu@v3
|
||||
if: github.repository == 'nushell/nightly'
|
||||
with:
|
||||
version: 0.98.0
|
||||
version: 0.101.0
|
||||
|
||||
# Synchronize the main branch of nightly repo with the main branch of Nushell official repo
|
||||
- name: Prepare for Nightly Release
|
||||
@ -114,7 +114,7 @@ jobs:
|
||||
- target: armv7-unknown-linux-musleabihf
|
||||
os: ubuntu-22.04
|
||||
- target: riscv64gc-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-22.04
|
||||
- target: loongarch64-unknown-linux-gnu
|
||||
os: ubuntu-22.04
|
||||
|
||||
@ -139,7 +139,7 @@ jobs:
|
||||
- name: Setup Nushell
|
||||
uses: hustcer/setup-nu@v3
|
||||
with:
|
||||
version: 0.98.0
|
||||
version: 0.101.0
|
||||
|
||||
- name: Release Nu Binary
|
||||
id: nu
|
||||
@ -170,7 +170,7 @@ jobs:
|
||||
# REF: https://github.com/marketplace/actions/gh-release
|
||||
# Create a release only in nushell/nightly repo
|
||||
- name: Publish Archive
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
uses: softprops/action-gh-release@v2.0.9
|
||||
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
|
||||
with:
|
||||
prerelease: true
|
||||
@ -197,7 +197,7 @@ jobs:
|
||||
- name: Setup Nushell
|
||||
uses: hustcer/setup-nu@v3
|
||||
with:
|
||||
version: 0.98.0
|
||||
version: 0.101.0
|
||||
|
||||
# Keep the last a few releases
|
||||
- name: Delete Older Releases
|
||||
|
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@ -7,7 +7,9 @@ name: Create Release Draft
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ["[0-9]+.[0-9]+.[0-9]+*"]
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+*'
|
||||
- '!*nightly*' # Don't trigger release for nightly tags
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@ -64,7 +66,7 @@ jobs:
|
||||
- target: armv7-unknown-linux-musleabihf
|
||||
os: ubuntu-22.04
|
||||
- target: riscv64gc-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-22.04
|
||||
- target: loongarch64-unknown-linux-gnu
|
||||
os: ubuntu-22.04
|
||||
|
||||
@ -87,7 +89,7 @@ jobs:
|
||||
- name: Setup Nushell
|
||||
uses: hustcer/setup-nu@v3
|
||||
with:
|
||||
version: 0.98.0
|
||||
version: 0.101.0
|
||||
|
||||
- name: Release Nu Binary
|
||||
id: nu
|
||||
@ -98,9 +100,10 @@ jobs:
|
||||
TARGET: ${{ matrix.target }}
|
||||
_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
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
uses: softprops/action-gh-release@v2.0.5
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
with:
|
||||
draft: true
|
||||
@ -124,7 +127,7 @@ jobs:
|
||||
- name: Create Checksums
|
||||
run: cd release && shasum -a 256 * > ../SHA256SUMS
|
||||
- name: Publish Checksums
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
uses: softprops/action-gh-release@v2.0.5
|
||||
with:
|
||||
draft: true
|
||||
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
|
||||
|
||||
- name: Check spelling
|
||||
uses: crate-ci/typos@v1.26.0
|
||||
uses: crate-ci/typos@v1.29.4
|
||||
|
3759
Cargo.lock
generated
3759
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
143
Cargo.toml
143
Cargo.toml
@ -10,8 +10,8 @@ homepage = "https://www.nushell.sh"
|
||||
license = "MIT"
|
||||
name = "nu"
|
||||
repository = "https://github.com/nushell/nushell"
|
||||
rust-version = "1.79.0"
|
||||
version = "0.99.1"
|
||||
rust-version = "1.82.0"
|
||||
version = "0.102.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -66,33 +66,33 @@ alphanumeric-sort = "1.5"
|
||||
ansi-str = "0.8"
|
||||
anyhow = "1.0.82"
|
||||
base64 = "0.22.1"
|
||||
bracoxide = "0.1.2"
|
||||
brotli = "5.0"
|
||||
bracoxide = "0.1.4"
|
||||
brotli = "7.0"
|
||||
byteorder = "1.5"
|
||||
bytes = "1"
|
||||
bytesize = "1.3"
|
||||
calamine = "0.24.0"
|
||||
calamine = "0.26.1"
|
||||
chardetng = "0.1.17"
|
||||
chrono = { default-features = false, version = "0.4.34" }
|
||||
chrono-humanize = "0.2.3"
|
||||
chrono-tz = "0.8"
|
||||
chrono-tz = "0.10"
|
||||
crossbeam-channel = "0.5.8"
|
||||
crossterm = "0.28.1"
|
||||
csv = "1.3"
|
||||
ctrlc = "3.4"
|
||||
devicons = "0.6.12"
|
||||
dialoguer = { default-features = false, version = "0.11" }
|
||||
digest = { default-features = false, version = "0.10" }
|
||||
dirs = "5.0"
|
||||
dirs-sys = "0.4"
|
||||
dtparse = "2.0"
|
||||
encoding_rs = "0.8"
|
||||
fancy-regex = "0.13"
|
||||
fancy-regex = "0.14"
|
||||
filesize = "0.2"
|
||||
filetime = "0.2"
|
||||
fuzzy-matcher = "0.3"
|
||||
heck = "0.5.0"
|
||||
human-date-parser = "0.2.0"
|
||||
indexmap = "2.6"
|
||||
indexmap = "2.7"
|
||||
indicatif = "0.17"
|
||||
interprocess = "2.2.0"
|
||||
is_executable = "1.0"
|
||||
@ -102,23 +102,25 @@ libproc = "0.14"
|
||||
log = "0.4"
|
||||
lru = "0.12"
|
||||
lscolors = { version = "0.17", default-features = false }
|
||||
lsp-server = "0.7.5"
|
||||
lsp-types = "0.95.0"
|
||||
lsp-server = "0.7.8"
|
||||
lsp-types = { version = "0.97.0", features = ["proposed"] }
|
||||
lsp-textdocument = "0.4.1"
|
||||
mach2 = "0.4"
|
||||
md5 = { version = "0.10", package = "md-5" }
|
||||
miette = "7.2"
|
||||
miette = "7.3"
|
||||
mime = "0.3.17"
|
||||
mime_guess = "2.0"
|
||||
mockito = { version = "1.5", default-features = false }
|
||||
multipart-rs = "0.1.11"
|
||||
mockito = { version = "1.6", default-features = false }
|
||||
multipart-rs = "0.1.13"
|
||||
native-tls = "0.2"
|
||||
nix = { version = "0.29", default-features = false }
|
||||
notify-debouncer-full = { version = "0.3", default-features = false }
|
||||
nu-ansi-term = "0.50.1"
|
||||
nucleo-matcher = "0.3"
|
||||
num-format = "0.4"
|
||||
num-traits = "0.2"
|
||||
oem_cp = "2.0.0"
|
||||
omnipath = "0.1"
|
||||
once_cell = "1.20"
|
||||
open = "5.3"
|
||||
os_pipe = { version = "1.2", features = ["io_safety"] }
|
||||
pathdiff = "0.2"
|
||||
@ -127,58 +129,62 @@ pretty_assertions = "1.4"
|
||||
print-positions = "0.6"
|
||||
proc-macro-error = { version = "1.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
procfs = "0.16.0"
|
||||
procfs = "0.17.0"
|
||||
pwd = "1.3"
|
||||
quick-xml = "0.32.0"
|
||||
quick-xml = "0.37.0"
|
||||
quickcheck = "1.0"
|
||||
quickcheck_macros = "1.0"
|
||||
quote = "1.0"
|
||||
rand = "0.8"
|
||||
getrandom = "0.2" # pick same version that rand requires
|
||||
rand_chacha = "0.3.1"
|
||||
ratatui = "0.26"
|
||||
rayon = "1.10"
|
||||
reedline = "0.36.0"
|
||||
regex = "1.9.5"
|
||||
reedline = "0.38.0"
|
||||
rmp = "0.8"
|
||||
rmp-serde = "1.3"
|
||||
ropey = "1.6.1"
|
||||
roxmltree = "0.19"
|
||||
rstest = { version = "0.18", default-features = false }
|
||||
roxmltree = "0.20"
|
||||
rstest = { version = "0.23", default-features = false }
|
||||
rstest_reuse = "0.7"
|
||||
rusqlite = "0.31"
|
||||
rust-embed = "8.5.0"
|
||||
scopeguard = { version = "1.2.0" }
|
||||
serde = { version = "1.0" }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde_yaml = "0.9"
|
||||
serde_yaml = "0.9.33"
|
||||
sha2 = "0.10"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
syn = "2.0"
|
||||
sysinfo = "0.30"
|
||||
tabled = { version = "0.16.0", default-features = false }
|
||||
tempfile = "3.13"
|
||||
terminal_size = "0.3"
|
||||
titlecase = "2.0"
|
||||
sysinfo = "0.33"
|
||||
tabled = { version = "0.17.0", default-features = false }
|
||||
tempfile = "3.15"
|
||||
titlecase = "3.0"
|
||||
toml = "0.8"
|
||||
trash = "3.3"
|
||||
trash = "5.2"
|
||||
update-informer = { version = "1.2.0", default-features = false, features = ["github", "native-tls", "ureq"] }
|
||||
umask = "2.1"
|
||||
unicode-segmentation = "1.12"
|
||||
unicode-width = "0.1"
|
||||
ureq = { version = "2.10", default-features = false }
|
||||
unicode-width = "0.2"
|
||||
ureq = { version = "2.12", default-features = false }
|
||||
url = "2.2"
|
||||
uu_cp = "0.0.27"
|
||||
uu_mkdir = "0.0.27"
|
||||
uu_mktemp = "0.0.27"
|
||||
uu_mv = "0.0.27"
|
||||
uu_whoami = "0.0.27"
|
||||
uu_uname = "0.0.27"
|
||||
uucore = "0.0.27"
|
||||
uuid = "1.10.0"
|
||||
uu_cp = "0.0.29"
|
||||
uu_mkdir = "0.0.29"
|
||||
uu_mktemp = "0.0.29"
|
||||
uu_mv = "0.0.29"
|
||||
uu_touch = "0.0.29"
|
||||
uu_whoami = "0.0.29"
|
||||
uu_uname = "0.0.29"
|
||||
uucore = "0.0.29"
|
||||
uuid = "1.12.0"
|
||||
v_htmlescape = "0.15.0"
|
||||
wax = "0.6"
|
||||
which = "6.0.0"
|
||||
windows = "0.54"
|
||||
web-time = "1.1.0"
|
||||
which = "7.0.0"
|
||||
windows = "0.56"
|
||||
windows-sys = "0.48"
|
||||
winreg = "0.52"
|
||||
memchr = "2.7.4"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# Warning: workspace lints affect library code as well as tests, so don't enable lints that would be too noisy in tests like that.
|
||||
@ -189,22 +195,22 @@ unchecked_duration_subtraction = "warn"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-cli = { path = "./crates/nu-cli", version = "0.99.1" }
|
||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.99.1" }
|
||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.99.1" }
|
||||
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.99.1", optional = true }
|
||||
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.99.1" }
|
||||
nu-command = { path = "./crates/nu-command", version = "0.99.1" }
|
||||
nu-engine = { path = "./crates/nu-engine", version = "0.99.1" }
|
||||
nu-explore = { path = "./crates/nu-explore", version = "0.99.1" }
|
||||
nu-lsp = { path = "./crates/nu-lsp/", version = "0.99.1" }
|
||||
nu-parser = { path = "./crates/nu-parser", version = "0.99.1" }
|
||||
nu-path = { path = "./crates/nu-path", version = "0.99.1" }
|
||||
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.99.1" }
|
||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.99.1" }
|
||||
nu-std = { path = "./crates/nu-std", version = "0.99.1" }
|
||||
nu-system = { path = "./crates/nu-system", version = "0.99.1" }
|
||||
nu-utils = { path = "./crates/nu-utils", version = "0.99.1" }
|
||||
nu-cli = { path = "./crates/nu-cli", version = "0.102.0" }
|
||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.102.0" }
|
||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.102.0" }
|
||||
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.102.0", optional = true }
|
||||
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.102.0" }
|
||||
nu-command = { path = "./crates/nu-command", version = "0.102.0" }
|
||||
nu-engine = { path = "./crates/nu-engine", version = "0.102.0" }
|
||||
nu-explore = { path = "./crates/nu-explore", version = "0.102.0" }
|
||||
nu-lsp = { path = "./crates/nu-lsp/", version = "0.102.0" }
|
||||
nu-parser = { path = "./crates/nu-parser", version = "0.102.0" }
|
||||
nu-path = { path = "./crates/nu-path", version = "0.102.0" }
|
||||
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.102.0" }
|
||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.102.0" }
|
||||
nu-std = { path = "./crates/nu-std", version = "0.102.0" }
|
||||
nu-system = { path = "./crates/nu-system", version = "0.102.0" }
|
||||
nu-utils = { path = "./crates/nu-utils", version = "0.102.0" }
|
||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||
|
||||
crossterm = { workspace = true }
|
||||
@ -234,27 +240,32 @@ nix = { workspace = true, default-features = false, features = [
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
nu-test-support = { path = "./crates/nu-test-support", version = "0.99.1" }
|
||||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.99.1" }
|
||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.99.1" }
|
||||
nu-test-support = { path = "./crates/nu-test-support", version = "0.102.0" }
|
||||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.102.0" }
|
||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.102.0" }
|
||||
assert_cmd = "2.0"
|
||||
dirs = { workspace = true }
|
||||
tango-bench = "0.6"
|
||||
pretty_assertions = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
fancy-regex = { workspace = true }
|
||||
rstest = { workspace = true, default-features = false }
|
||||
serial_test = "3.1"
|
||||
serial_test = "3.2"
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
plugin = [
|
||||
"nu-plugin-engine",
|
||||
# crates
|
||||
"nu-cmd-plugin",
|
||||
"nu-plugin-engine",
|
||||
|
||||
# features
|
||||
"nu-cli/plugin",
|
||||
"nu-parser/plugin",
|
||||
"nu-cmd-lang/plugin",
|
||||
"nu-command/plugin",
|
||||
"nu-protocol/plugin",
|
||||
"nu-engine/plugin",
|
||||
"nu-engine/plugin",
|
||||
"nu-parser/plugin",
|
||||
"nu-protocol/plugin",
|
||||
]
|
||||
|
||||
default = [
|
||||
@ -320,4 +331,4 @@ bench = false
|
||||
# Run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse`
|
||||
[[bench]]
|
||||
name = "benchmarks"
|
||||
harness = false
|
||||
harness = false
|
||||
|
116
README.md
116
README.md
@ -58,7 +58,7 @@ For details about which platforms the Nushell team actively supports, see [our p
|
||||
|
||||
## 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.
|
||||
|
||||
It sets all of the default configuration to run Nushell. From here one can
|
||||
@ -95,44 +95,44 @@ Commands that work in the pipeline fit into one of three categories:
|
||||
Commands are separated by the pipe symbol (`|`) to denote a pipeline flowing left to right.
|
||||
|
||||
```shell
|
||||
> ls | where type == "dir" | table
|
||||
╭────┬──────────┬──────┬─────────┬───────────────╮
|
||||
│ # │ name │ type │ size │ modified │
|
||||
├────┼──────────┼──────┼─────────┼───────────────┤
|
||||
│ 0 │ .cargo │ dir │ 0 B │ 9 minutes ago │
|
||||
│ 1 │ assets │ dir │ 0 B │ 2 weeks ago │
|
||||
│ 2 │ crates │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||
│ 3 │ docker │ dir │ 0 B │ 2 weeks ago │
|
||||
│ 4 │ docs │ dir │ 0 B │ 2 weeks ago │
|
||||
│ 5 │ images │ dir │ 0 B │ 2 weeks ago │
|
||||
│ 6 │ pkg_mgrs │ dir │ 0 B │ 2 weeks ago │
|
||||
│ 7 │ samples │ dir │ 0 B │ 2 weeks ago │
|
||||
│ 8 │ src │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||
│ 9 │ target │ dir │ 0 B │ a day ago │
|
||||
│ 10 │ tests │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||
│ 11 │ wix │ dir │ 0 B │ 2 weeks ago │
|
||||
╰────┴──────────┴──────┴─────────┴───────────────╯
|
||||
ls | where type == "dir" | table
|
||||
# => ╭────┬──────────┬──────┬─────────┬───────────────╮
|
||||
# => │ # │ name │ type │ size │ modified │
|
||||
# => ├────┼──────────┼──────┼─────────┼───────────────┤
|
||||
# => │ 0 │ .cargo │ dir │ 0 B │ 9 minutes ago │
|
||||
# => │ 1 │ assets │ dir │ 0 B │ 2 weeks ago │
|
||||
# => │ 2 │ crates │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||
# => │ 3 │ docker │ dir │ 0 B │ 2 weeks ago │
|
||||
# => │ 4 │ docs │ dir │ 0 B │ 2 weeks ago │
|
||||
# => │ 5 │ images │ dir │ 0 B │ 2 weeks ago │
|
||||
# => │ 6 │ pkg_mgrs │ dir │ 0 B │ 2 weeks ago │
|
||||
# => │ 7 │ samples │ dir │ 0 B │ 2 weeks ago │
|
||||
# => │ 8 │ src │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||
# => │ 9 │ target │ dir │ 0 B │ a day ago │
|
||||
# => │ 10 │ tests │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||
# => │ 11 │ wix │ dir │ 0 B │ 2 weeks ago │
|
||||
# => ╰────┴──────────┴──────┴─────────┴───────────────╯
|
||||
```
|
||||
|
||||
Because most of the time you'll want to see the output of a pipeline, `table` is assumed.
|
||||
We could have also written the above:
|
||||
|
||||
```shell
|
||||
> ls | where type == "dir"
|
||||
ls | where type == "dir"
|
||||
```
|
||||
|
||||
Being able to use the same commands and compose them differently is an important philosophy in Nu.
|
||||
For example, we could use the built-in `ps` command to get a list of the running processes, using the same `where` as above.
|
||||
|
||||
```shell
|
||||
> ps | where cpu > 0
|
||||
╭───┬───────┬───────────┬───────┬───────────┬───────────╮
|
||||
│ # │ pid │ name │ cpu │ mem │ virtual │
|
||||
├───┼───────┼───────────┼───────┼───────────┼───────────┤
|
||||
│ 0 │ 2240 │ Slack.exe │ 16.40 │ 178.3 MiB │ 232.6 MiB │
|
||||
│ 1 │ 16948 │ Slack.exe │ 16.32 │ 205.0 MiB │ 197.9 MiB │
|
||||
│ 2 │ 17700 │ nu.exe │ 3.77 │ 26.1 MiB │ 8.8 MiB │
|
||||
╰───┴───────┴───────────┴───────┴───────────┴───────────╯
|
||||
ps | where cpu > 0
|
||||
# => ╭───┬───────┬───────────┬───────┬───────────┬───────────╮
|
||||
# => │ # │ pid │ name │ cpu │ mem │ virtual │
|
||||
# => ├───┼───────┼───────────┼───────┼───────────┼───────────┤
|
||||
# => │ 0 │ 2240 │ Slack.exe │ 16.40 │ 178.3 MiB │ 232.6 MiB │
|
||||
# => │ 1 │ 16948 │ Slack.exe │ 16.32 │ 205.0 MiB │ 197.9 MiB │
|
||||
# => │ 2 │ 17700 │ nu.exe │ 3.77 │ 26.1 MiB │ 8.8 MiB │
|
||||
# => ╰───┴───────┴───────────┴───────┴───────────┴───────────╯
|
||||
```
|
||||
|
||||
### Opening files
|
||||
@ -141,46 +141,46 @@ Nu can load file and URL contents as raw text or structured data (if it recogniz
|
||||
For example, you can load a .toml file as structured data and explore it:
|
||||
|
||||
```shell
|
||||
> open Cargo.toml
|
||||
╭──────────────────┬────────────────────╮
|
||||
│ bin │ [table 1 row] │
|
||||
│ dependencies │ {record 25 fields} │
|
||||
│ dev-dependencies │ {record 8 fields} │
|
||||
│ features │ {record 10 fields} │
|
||||
│ package │ {record 13 fields} │
|
||||
│ patch │ {record 1 field} │
|
||||
│ profile │ {record 3 fields} │
|
||||
│ target │ {record 3 fields} │
|
||||
│ workspace │ {record 1 field} │
|
||||
╰──────────────────┴────────────────────╯
|
||||
open Cargo.toml
|
||||
# => ╭──────────────────┬────────────────────╮
|
||||
# => │ bin │ [table 1 row] │
|
||||
# => │ dependencies │ {record 25 fields} │
|
||||
# => │ dev-dependencies │ {record 8 fields} │
|
||||
# => │ features │ {record 10 fields} │
|
||||
# => │ package │ {record 13 fields} │
|
||||
# => │ patch │ {record 1 field} │
|
||||
# => │ profile │ {record 3 fields} │
|
||||
# => │ target │ {record 3 fields} │
|
||||
# => │ workspace │ {record 1 field} │
|
||||
# => ╰──────────────────┴────────────────────╯
|
||||
```
|
||||
|
||||
We can pipe this into a command that gets the contents of one of the columns:
|
||||
|
||||
```shell
|
||||
> open Cargo.toml | get package
|
||||
╭───────────────┬────────────────────────────────────╮
|
||||
│ authors │ [list 1 item] │
|
||||
│ default-run │ nu │
|
||||
│ description │ A new type of shell │
|
||||
│ documentation │ https://www.nushell.sh/book/ │
|
||||
│ edition │ 2018 │
|
||||
│ exclude │ [list 1 item] │
|
||||
│ homepage │ https://www.nushell.sh │
|
||||
│ license │ MIT │
|
||||
│ metadata │ {record 1 field} │
|
||||
│ name │ nu │
|
||||
│ repository │ https://github.com/nushell/nushell │
|
||||
│ rust-version │ 1.60 │
|
||||
│ version │ 0.72.0 │
|
||||
╰───────────────┴────────────────────────────────────╯
|
||||
open Cargo.toml | get package
|
||||
# => ╭───────────────┬────────────────────────────────────╮
|
||||
# => │ authors │ [list 1 item] │
|
||||
# => │ default-run │ nu │
|
||||
# => │ description │ A new type of shell │
|
||||
# => │ documentation │ https://www.nushell.sh/book/ │
|
||||
# => │ edition │ 2018 │
|
||||
# => │ exclude │ [list 1 item] │
|
||||
# => │ homepage │ https://www.nushell.sh │
|
||||
# => │ license │ MIT │
|
||||
# => │ metadata │ {record 1 field} │
|
||||
# => │ name │ nu │
|
||||
# => │ repository │ https://github.com/nushell/nushell │
|
||||
# => │ rust-version │ 1.60 │
|
||||
# => │ version │ 0.72.0 │
|
||||
# => ╰───────────────┴────────────────────────────────────╯
|
||||
```
|
||||
|
||||
And if needed we can drill down further:
|
||||
|
||||
```shell
|
||||
> open Cargo.toml | get package.version
|
||||
0.72.0
|
||||
open Cargo.toml | get package.version
|
||||
# => 0.72.0
|
||||
```
|
||||
|
||||
### Plugins
|
||||
@ -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!
|
||||
|
||||
<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>
|
||||
|
||||
## License
|
||||
|
@ -46,9 +46,6 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
|
||||
|
||||
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(
|
||||
&commands,
|
||||
&mut engine,
|
||||
|
3
clippy/wasm/clippy.toml
Normal file
3
clippy/wasm/clippy.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[[disallowed-types]]
|
||||
path = "std::time::Instant"
|
||||
reason = "WASM panics if used, use `web_time::Instant` instead"
|
@ -5,39 +5,39 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cli"
|
||||
version = "0.99.1"
|
||||
version = "0.102.0"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[dev-dependencies]
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.99.1" }
|
||||
nu-command = { path = "../nu-command", version = "0.99.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.99.1" }
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.102.0" }
|
||||
nu-command = { path = "../nu-command", version = "0.102.0" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.102.0" }
|
||||
rstest = { workspace = true, default-features = false }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.99.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
||||
nu-path = { path = "../nu-path", version = "0.99.1" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.99.1", optional = true }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.99.1" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.99.1" }
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.102.0" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.102.0", features = ["os"] }
|
||||
nu-glob = { path = "../nu-glob", version = "0.102.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.102.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.102.0" }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.102.0", optional = true }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.102.0", features = ["os"] }
|
||||
nu-utils = { path = "../nu-utils", version = "0.102.0" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.102.0" }
|
||||
nu-ansi-term = { workspace = true }
|
||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||
|
||||
chrono = { default-features = false, features = ["std"], workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
fancy-regex = { workspace = true }
|
||||
fuzzy-matcher = { workspace = true }
|
||||
is_executable = { workspace = true }
|
||||
log = { workspace = true }
|
||||
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
|
||||
once_cell = { workspace = true }
|
||||
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||
nucleo-matcher = { workspace = true }
|
||||
percent-encoding = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
unicode-segmentation = { workspace = true }
|
||||
|
@ -17,6 +17,7 @@ pub fn add_cli_context(mut engine_state: EngineState) -> EngineState {
|
||||
CommandlineGetCursor,
|
||||
CommandlineSetCursor,
|
||||
History,
|
||||
HistoryImport,
|
||||
HistorySession,
|
||||
Keybindings,
|
||||
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";
|
@ -1,10 +1,12 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::HistoryFileFormat;
|
||||
use nu_protocol::{shell_error::io::IoError, HistoryFileFormat};
|
||||
use reedline::{
|
||||
FileBackedHistory, History as ReedlineHistory, HistoryItem, SearchDirection, SearchQuery,
|
||||
SqliteBackedHistory,
|
||||
};
|
||||
|
||||
use super::fields;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct History;
|
||||
|
||||
@ -83,17 +85,19 @@ impl Command for History {
|
||||
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
||||
Value::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),
|
||||
},
|
||||
head,
|
||||
)
|
||||
})
|
||||
})
|
||||
.ok_or(ShellError::FileNotFound {
|
||||
file: history_path.display().to_string(),
|
||||
span: head,
|
||||
})?
|
||||
.ok_or(IoError::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
head,
|
||||
history_path,
|
||||
))?
|
||||
.into_pipeline_data(head, signals)),
|
||||
HistoryFileFormat::Sqlite => Ok(history_reader
|
||||
.and_then(|h| {
|
||||
@ -106,10 +110,11 @@ impl Command for History {
|
||||
.enumerate()
|
||||
.map(move |(idx, entry)| create_history_record(idx, entry, long, head))
|
||||
})
|
||||
.ok_or(ShellError::FileNotFound {
|
||||
file: history_path.display().to_string(),
|
||||
span: head,
|
||||
})?
|
||||
.ok_or(IoError::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
head,
|
||||
history_path,
|
||||
))?
|
||||
.into_pipeline_data(head, signals)),
|
||||
}
|
||||
}
|
||||
@ -176,13 +181,13 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span)
|
||||
Value::record(
|
||||
record! {
|
||||
"item_id" => item_id_value,
|
||||
"start_timestamp" => start_timestamp_value,
|
||||
"command" => command_value,
|
||||
"session_id" => session_id_value,
|
||||
"hostname" => hostname_value,
|
||||
"cwd" => cwd_value,
|
||||
"duration" => duration_value,
|
||||
"exit_status" => exit_status_value,
|
||||
fields::START_TIMESTAMP => start_timestamp_value,
|
||||
fields::COMMAND_LINE => command_value,
|
||||
fields::SESSION_ID => session_id_value,
|
||||
fields::HOSTNAME => hostname_value,
|
||||
fields::CWD => cwd_value,
|
||||
fields::DURATION => duration_value,
|
||||
fields::EXIT_STATUS => exit_status_value,
|
||||
"idx" => index_value,
|
||||
},
|
||||
head,
|
||||
@ -190,11 +195,11 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span)
|
||||
} else {
|
||||
Value::record(
|
||||
record! {
|
||||
"start_timestamp" => start_timestamp_value,
|
||||
"command" => command_value,
|
||||
"cwd" => cwd_value,
|
||||
"duration" => duration_value,
|
||||
"exit_status" => exit_status_value,
|
||||
fields::START_TIMESTAMP => start_timestamp_value,
|
||||
fields::COMMAND_LINE => command_value,
|
||||
fields::CWD => cwd_value,
|
||||
fields::DURATION => duration_value,
|
||||
fields::EXIT_STATUS => exit_status_value,
|
||||
},
|
||||
head,
|
||||
)
|
||||
|
441
crates/nu-cli/src/commands/history/history_import.rs
Normal file
441
crates/nu-cli/src/commands/history/history_import.rs
Normal file
@ -0,0 +1,441 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{
|
||||
shell_error::{self, io::IoError},
|
||||
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::String, 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 span = call.head;
|
||||
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: span.into() });
|
||||
};
|
||||
if let Some(bak_path) = backup(¤t_history_path, span)? {
|
||||
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, |v| duration_from_value(v, span))?,
|
||||
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, span: Span) -> Result<std::time::Duration, ShellError> {
|
||||
chrono::Duration::nanoseconds(v.as_duration()?)
|
||||
.to_std()
|
||||
.map_err(|_| ShellError::NeedsPositiveValue { span })
|
||||
}
|
||||
|
||||
fn find_backup_path(path: &Path, span: Span) -> 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::GenericError {
|
||||
error: "History path not UTF-8".to_string(),
|
||||
msg: "History path must be representable as UTF-8".to_string(),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
});
|
||||
};
|
||||
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::GenericError {
|
||||
error: "Too many backup files".to_string(),
|
||||
msg: "Found too many existing backup files".to_string(),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn backup(path: &Path, span: Span) -> Result<Option<PathBuf>, ShellError> {
|
||||
match path.metadata() {
|
||||
Ok(md) if md.is_file() => (),
|
||||
Ok(_) => {
|
||||
return Err(IoError::new_with_additional_context(
|
||||
shell_error::io::ErrorKind::NotAFile,
|
||||
span,
|
||||
PathBuf::from(path),
|
||||
"history path exists but is not a file",
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => {
|
||||
return Err(IoError::new_internal(
|
||||
e.kind(),
|
||||
"Could not get metadata",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
let bak_path = find_backup_path(path, span)?;
|
||||
std::fs::copy(path, &bak_path).map_err(|err| {
|
||||
IoError::new_internal(
|
||||
err.kind(),
|
||||
"Could not copy backup",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
})?;
|
||||
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"), Span::test_data()).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"), Span::test_data()),
|
||||
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"), Span::test_data()).unwrap();
|
||||
assert!(bak_path.is_none());
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
mod fields;
|
||||
mod history_;
|
||||
mod history_import;
|
||||
mod history_session;
|
||||
|
||||
pub use history_::History;
|
||||
pub use history_import::HistoryImport;
|
||||
pub use history_session::HistorySession;
|
||||
|
@ -2,6 +2,7 @@ use crossterm::{
|
||||
event::Event, event::KeyCode, event::KeyEvent, execute, terminal, QueueableCommand,
|
||||
};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::shell_error::io::IoError;
|
||||
use std::io::{stdout, Write};
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -39,7 +40,13 @@ impl Command for KeybindingsListen {
|
||||
match print_events(engine_state) {
|
||||
Ok(v) => Ok(v.into_pipeline_data()),
|
||||
Err(e) => {
|
||||
terminal::disable_raw_mode()?;
|
||||
terminal::disable_raw_mode().map_err(|err| {
|
||||
IoError::new_internal(
|
||||
err.kind(),
|
||||
"Could not disable raw mode",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
})?;
|
||||
Err(ShellError::GenericError {
|
||||
error: "Error with input".into(),
|
||||
msg: "".into(),
|
||||
@ -63,8 +70,20 @@ impl Command for KeybindingsListen {
|
||||
pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
|
||||
let config = engine_state.get_config();
|
||||
|
||||
stdout().flush()?;
|
||||
terminal::enable_raw_mode()?;
|
||||
stdout().flush().map_err(|err| {
|
||||
IoError::new_internal(
|
||||
err.kind(),
|
||||
"Could not flush stdout",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
})?;
|
||||
terminal::enable_raw_mode().map_err(|err| {
|
||||
IoError::new_internal(
|
||||
err.kind(),
|
||||
"Could not enable raw mode",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if config.use_kitty_protocol {
|
||||
if let Ok(false) = crossterm::terminal::supports_keyboard_enhancement() {
|
||||
@ -94,7 +113,9 @@ pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
|
||||
let mut stdout = std::io::BufWriter::new(std::io::stderr());
|
||||
|
||||
loop {
|
||||
let event = crossterm::event::read()?;
|
||||
let event = crossterm::event::read().map_err(|err| {
|
||||
IoError::new_internal(err.kind(), "Could not read event", nu_protocol::location!())
|
||||
})?;
|
||||
if event == Event::Key(KeyCode::Esc.into()) {
|
||||
break;
|
||||
}
|
||||
@ -113,9 +134,25 @@ pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
|
||||
|
||||
_ => "".to_string(),
|
||||
};
|
||||
stdout.queue(crossterm::style::Print(o))?;
|
||||
stdout.queue(crossterm::style::Print("\r\n"))?;
|
||||
stdout.flush()?;
|
||||
stdout.queue(crossterm::style::Print(o)).map_err(|err| {
|
||||
IoError::new_internal(
|
||||
err.kind(),
|
||||
"Could not print output record",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
})?;
|
||||
stdout
|
||||
.queue(crossterm::style::Print("\r\n"))
|
||||
.map_err(|err| {
|
||||
IoError::new_internal(
|
||||
err.kind(),
|
||||
"Could not print linebreak",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
})?;
|
||||
stdout.flush().map_err(|err| {
|
||||
IoError::new_internal(err.kind(), "Could not flush", nu_protocol::location!())
|
||||
})?;
|
||||
}
|
||||
|
||||
if config.use_kitty_protocol {
|
||||
@ -125,7 +162,13 @@ pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
|
||||
);
|
||||
}
|
||||
|
||||
terminal::disable_raw_mode()?;
|
||||
terminal::disable_raw_mode().map_err(|err| {
|
||||
IoError::new_internal(
|
||||
err.kind(),
|
||||
"Could not disable raw mode",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Value::nothing(Span::unknown()))
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ mod keybindings_list;
|
||||
mod keybindings_listen;
|
||||
|
||||
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_default::KeybindingsDefault;
|
||||
pub use keybindings_list::KeybindingsList;
|
||||
|
@ -31,6 +31,7 @@ pub struct SemanticSuggestion {
|
||||
pub enum SuggestionKind {
|
||||
Command(nu_protocol::engine::CommandType),
|
||||
Type(nu_protocol::Type),
|
||||
Module,
|
||||
}
|
||||
|
||||
impl From<Suggestion> for SemanticSuggestion {
|
||||
|
@ -1,5 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
completions::{Completer, CompletionOptions, MatchAlgorithm},
|
||||
completions::{Completer, CompletionOptions},
|
||||
SuggestionKind,
|
||||
};
|
||||
use nu_parser::FlatShape;
|
||||
@ -9,7 +11,7 @@ use nu_protocol::{
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
|
||||
use super::{completion_common::sort_suggestions, SemanticSuggestion};
|
||||
use super::{completion_options::NuMatcher, SemanticSuggestion};
|
||||
|
||||
pub struct CommandCompletion {
|
||||
flattened: Vec<(Span, FlatShape)>,
|
||||
@ -33,15 +35,15 @@ impl CommandCompletion {
|
||||
fn external_command_completion(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
prefix: &str,
|
||||
match_algorithm: MatchAlgorithm,
|
||||
) -> Vec<String> {
|
||||
let mut executables = vec![];
|
||||
sugg_span: reedline::Span,
|
||||
matched_internal: impl Fn(&str) -> bool,
|
||||
matcher: &mut NuMatcher<String>,
|
||||
) -> HashMap<String, SemanticSuggestion> {
|
||||
let mut suggs = HashMap::new();
|
||||
|
||||
// os agnostic way to get the PATH env var
|
||||
let paths = working_set.permanent_state.get_path_env_var();
|
||||
let paths = working_set.permanent_state.get_env_var_insensitive("path");
|
||||
|
||||
if let Some(paths) = paths {
|
||||
if let Some((_, paths)) = paths {
|
||||
if let Ok(paths) = paths.as_list() {
|
||||
for path in paths {
|
||||
let path = path.coerce_str().unwrap_or_default();
|
||||
@ -54,24 +56,38 @@ impl CommandCompletion {
|
||||
.completions
|
||||
.external
|
||||
.max_results
|
||||
> executables.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())
|
||||
<= suggs.len() as i64
|
||||
{
|
||||
if let Ok(name) = item.file_name().into_string() {
|
||||
executables.push(name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
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(
|
||||
@ -88,68 +104,59 @@ impl CommandCompletion {
|
||||
span: Span,
|
||||
offset: usize,
|
||||
find_externals: bool,
|
||||
match_algorithm: MatchAlgorithm,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
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
|
||||
.find_commands_by_predicate(filter_predicate, true)
|
||||
.into_iter()
|
||||
.map(move |x| SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: String::from_utf8_lossy(&x.0).to_string(),
|
||||
description: x.1,
|
||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
||||
append_whitespace: true,
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::Command(x.2)),
|
||||
})
|
||||
.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 {
|
||||
let mut internal_suggs = HashMap::new();
|
||||
let filtered_commands = working_set.find_commands_by_predicate(
|
||||
|name| {
|
||||
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 {
|
||||
value: x,
|
||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
||||
value: name.to_string(),
|
||||
description,
|
||||
span: sugg_span,
|
||||
append_whitespace: true,
|
||||
..Suggestion::default()
|
||||
},
|
||||
// TODO: is there a way to create a test?
|
||||
kind: None,
|
||||
});
|
||||
|
||||
let results_strings: Vec<String> =
|
||||
results.iter().map(|x| x.suggestion.value.clone()).collect();
|
||||
|
||||
for external in results_external {
|
||||
if results_strings.contains(&external.suggestion.value) {
|
||||
results.push(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: format!("^{}", external.suggestion.value),
|
||||
span: external.suggestion.span,
|
||||
append_whitespace: true,
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: external.kind,
|
||||
})
|
||||
} else {
|
||||
results.push(external)
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
} else {
|
||||
results
|
||||
kind: Some(SuggestionKind::Command(typ)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let mut external_suggs = if find_externals {
|
||||
self.external_command_completion(
|
||||
working_set,
|
||||
sugg_span,
|
||||
|name| internal_suggs.contains_key(name),
|
||||
&mut matcher,
|
||||
)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
let mut res = Vec::new();
|
||||
for cmd_name in matcher.results() {
|
||||
if let Some(sugg) = internal_suggs
|
||||
.remove(&cmd_name)
|
||||
.or_else(|| external_suggs.remove(&cmd_name))
|
||||
{
|
||||
res.push(sugg);
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +165,7 @@ impl Completer for CommandCompletion {
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
prefix: &[u8],
|
||||
_prefix: &[u8],
|
||||
span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
@ -188,18 +195,18 @@ impl Completer for CommandCompletion {
|
||||
Span::new(last.0.start, pos),
|
||||
offset,
|
||||
false,
|
||||
options.match_algorithm,
|
||||
options,
|
||||
)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
if !subcommands.is_empty() {
|
||||
return sort_suggestions(&String::from_utf8_lossy(prefix), subcommands, options);
|
||||
return subcommands;
|
||||
}
|
||||
|
||||
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(_))
|
||||
|| ((span.end - span.start) == 0)
|
||||
|| is_passthrough_command(working_set.delta.get_file_contents())
|
||||
@ -214,13 +221,11 @@ impl Completer for CommandCompletion {
|
||||
span,
|
||||
offset,
|
||||
config.completions.external.enable,
|
||||
options.match_algorithm,
|
||||
options,
|
||||
)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
sort_suggestions(&String::from_utf8_lossy(prefix), commands, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ use crate::completions::{
|
||||
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
|
||||
DotNuCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion,
|
||||
};
|
||||
use log::debug;
|
||||
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::{flatten_pipeline_element, parse, FlatShape};
|
||||
@ -52,6 +53,11 @@ impl NuCompleter {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
debug!(
|
||||
"process_completion: prefix: {}, new_span: {new_span:?}, offset: {offset}, pos: {pos}",
|
||||
String::from_utf8_lossy(prefix)
|
||||
);
|
||||
|
||||
completer.fetch(
|
||||
working_set,
|
||||
&self.stack,
|
||||
@ -99,18 +105,24 @@ impl NuCompleter {
|
||||
);
|
||||
|
||||
match result.and_then(|data| data.into_value(span)) {
|
||||
Ok(value) => {
|
||||
if let Value::List { vals, .. } = value {
|
||||
let result =
|
||||
map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
|
||||
|
||||
return Some(result);
|
||||
}
|
||||
Ok(Value::List { vals, .. }) => {
|
||||
let result =
|
||||
map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
|
||||
Some(result)
|
||||
}
|
||||
Ok(Value::Nothing { .. }) => None,
|
||||
Ok(value) => {
|
||||
log::error!(
|
||||
"External completer returned invalid value of type {}",
|
||||
value.get_type().to_string()
|
||||
);
|
||||
Some(vec![])
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to eval completer block: {err}");
|
||||
Some(vec![])
|
||||
}
|
||||
Err(err) => println!("failed to eval completer block: {err}"),
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
|
||||
@ -128,9 +140,29 @@ impl NuCompleter {
|
||||
|
||||
let config = self.engine_state.get_config();
|
||||
|
||||
let output = parse(&mut working_set, Some("completer"), line.as_bytes(), false);
|
||||
let outermost_block = parse(&mut working_set, Some("completer"), line.as_bytes(), false);
|
||||
|
||||
for pipeline in &output.pipelines {
|
||||
// Try to get the innermost block parsed (by span) so that we consider the correct context/scope.
|
||||
let target_block = working_set
|
||||
.delta
|
||||
.blocks
|
||||
.iter()
|
||||
.filter_map(|block| match block.span {
|
||||
Some(span) if span.contains(pos) => Some((block, span)),
|
||||
_ => None,
|
||||
})
|
||||
.reduce(|prev, cur| {
|
||||
// |(block, span), (block, span)|
|
||||
match cur.1.start.cmp(&prev.1.start) {
|
||||
core::cmp::Ordering::Greater => cur,
|
||||
core::cmp::Ordering::Equal if cur.1.end < prev.1.end => cur,
|
||||
_ => prev,
|
||||
}
|
||||
})
|
||||
.map(|(block, _)| block)
|
||||
.unwrap_or(&outermost_block);
|
||||
|
||||
for pipeline in &target_block.pipelines {
|
||||
for pipeline_element in &pipeline.elements {
|
||||
let flattened = flatten_pipeline_element(&working_set, pipeline_element);
|
||||
let mut spans: Vec<String> = vec![];
|
||||
@ -140,10 +172,10 @@ impl NuCompleter {
|
||||
.first()
|
||||
.filter(|content| content.as_str() == "sudo" || content.as_str() == "doas")
|
||||
.is_some();
|
||||
// Read the current spam to string
|
||||
let current_span = working_set.get_span_contents(flat.0).to_vec();
|
||||
let current_span_str = String::from_utf8_lossy(¤t_span);
|
||||
|
||||
// Read the current span to string
|
||||
let current_span = working_set.get_span_contents(flat.0);
|
||||
let current_span_str = String::from_utf8_lossy(current_span);
|
||||
let is_last_span = pos >= flat.0.start && pos < flat.0.end;
|
||||
|
||||
// Skip the last 'a' as span item
|
||||
@ -170,9 +202,8 @@ impl NuCompleter {
|
||||
let new_span = Span::new(flat.0.start, flat.0.end - 1);
|
||||
|
||||
// Parses the prefix. Completion should look up to the cursor position, not after.
|
||||
let mut prefix = working_set.get_span_contents(flat.0);
|
||||
let index = pos - flat.0.start;
|
||||
prefix = &prefix[..index];
|
||||
let prefix = ¤t_span[..index];
|
||||
|
||||
// Variables completion
|
||||
if prefix.starts_with(b"$") || most_left_var.is_some() {
|
||||
@ -297,7 +328,7 @@ impl NuCompleter {
|
||||
let mut completer =
|
||||
OperatorCompletion::new(pipeline_element.expr.clone());
|
||||
|
||||
return self.process_completion(
|
||||
let operator_suggestion = self.process_completion(
|
||||
&mut completer,
|
||||
&working_set,
|
||||
prefix,
|
||||
@ -305,6 +336,9 @@ impl NuCompleter {
|
||||
fake_offset,
|
||||
pos,
|
||||
);
|
||||
if !operator_suggestion.is_empty() {
|
||||
return operator_suggestion;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -316,6 +350,7 @@ impl NuCompleter {
|
||||
self.stack.clone(),
|
||||
*decl_id,
|
||||
initial_line,
|
||||
FileCompletion::new(),
|
||||
);
|
||||
|
||||
return self.process_completion(
|
||||
|
@ -1,22 +1,20 @@
|
||||
use super::MatchAlgorithm;
|
||||
use crate::{
|
||||
completions::{matches, CompletionOptions},
|
||||
SemanticSuggestion,
|
||||
};
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
use super::{completion_options::NuMatcher, MatchAlgorithm};
|
||||
use crate::completions::CompletionOptions;
|
||||
use nu_ansi_term::Style;
|
||||
use nu_engine::env_to_string;
|
||||
use nu_path::dots::expand_ndots;
|
||||
use nu_path::{expand_to_real_path, home_dir};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
CompletionSort, Span,
|
||||
Span,
|
||||
};
|
||||
use nu_utils::get_ls_colors;
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PathBuiltFromString {
|
||||
cwd: PathBuf,
|
||||
parts: Vec<String>,
|
||||
isdir: bool,
|
||||
}
|
||||
@ -30,76 +28,84 @@ pub struct PathBuiltFromString {
|
||||
/// want_directory: Whether we want only directories as completion matches.
|
||||
/// Some commands like `cd` can only be run on directories whereas others
|
||||
/// like `ls` can be run on regular files as well.
|
||||
pub fn complete_rec(
|
||||
fn complete_rec(
|
||||
partial: &[&str],
|
||||
built: &PathBuiltFromString,
|
||||
cwd: &Path,
|
||||
built_paths: &[PathBuiltFromString],
|
||||
options: &CompletionOptions,
|
||||
want_directory: bool,
|
||||
isdir: bool,
|
||||
) -> Vec<PathBuiltFromString> {
|
||||
let mut completions = vec![];
|
||||
|
||||
if let Some((&base, rest)) = partial.split_first() {
|
||||
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
||||
let mut built = built.clone();
|
||||
built.parts.push(base.to_string());
|
||||
built.isdir = true;
|
||||
return complete_rec(rest, &built, cwd, options, want_directory, isdir);
|
||||
}
|
||||
}
|
||||
|
||||
let mut built_path = cwd.to_path_buf();
|
||||
for part in &built.parts {
|
||||
built_path.push(part);
|
||||
}
|
||||
|
||||
let Ok(result) = built_path.read_dir() else {
|
||||
return completions;
|
||||
};
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for entry in result.filter_map(|e| e.ok()) {
|
||||
let entry_name = entry.file_name().to_string_lossy().into_owned();
|
||||
let entry_isdir = entry.path().is_dir();
|
||||
let mut built = built.clone();
|
||||
built.parts.push(entry_name.clone());
|
||||
built.isdir = entry_isdir;
|
||||
|
||||
if !want_directory || entry_isdir {
|
||||
entries.push((entry_name, built));
|
||||
let built_paths: Vec<_> = built_paths
|
||||
.iter()
|
||||
.map(|built| {
|
||||
let mut built = built.clone();
|
||||
built.parts.push(base.to_string());
|
||||
built.isdir = true;
|
||||
built
|
||||
})
|
||||
.collect();
|
||||
return complete_rec(rest, &built_paths, options, want_directory, isdir);
|
||||
}
|
||||
}
|
||||
|
||||
let prefix = partial.first().unwrap_or(&"");
|
||||
let sorted_entries = sort_completions(prefix, entries, options, |(entry, _)| entry);
|
||||
let mut matcher = NuMatcher::new(prefix, options.clone());
|
||||
|
||||
for (entry_name, built) in sorted_entries {
|
||||
for built in built_paths {
|
||||
let mut path = built.cwd.clone();
|
||||
for part in &built.parts {
|
||||
path.push(part);
|
||||
}
|
||||
|
||||
let Ok(result) = path.read_dir() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for entry in result.filter_map(|e| e.ok()) {
|
||||
let entry_name = entry.file_name().to_string_lossy().into_owned();
|
||||
let entry_isdir = entry.path().is_dir() && !entry.path().is_symlink();
|
||||
let mut built = built.clone();
|
||||
built.parts.push(entry_name.clone());
|
||||
built.isdir = entry_isdir;
|
||||
|
||||
if !want_directory || entry_isdir {
|
||||
matcher.add(entry_name.clone(), (entry_name, built));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut completions = vec![];
|
||||
for (entry_name, built) in matcher.results() {
|
||||
match partial.split_first() {
|
||||
Some((base, rest)) => {
|
||||
if matches(base, &entry_name, options) {
|
||||
// We use `isdir` to confirm that the current component has
|
||||
// at least one next component or a slash.
|
||||
// Serves as confirmation to ignore longer completions for
|
||||
// components in between.
|
||||
if !rest.is_empty() || isdir {
|
||||
completions.extend(complete_rec(
|
||||
rest,
|
||||
&built,
|
||||
cwd,
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
));
|
||||
} else {
|
||||
completions.push(built);
|
||||
}
|
||||
// We use `isdir` to confirm that the current component has
|
||||
// at least one next component or a slash.
|
||||
// Serves as confirmation to ignore longer completions for
|
||||
// components in between.
|
||||
if !rest.is_empty() || isdir {
|
||||
completions.extend(complete_rec(
|
||||
rest,
|
||||
&[built],
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
));
|
||||
} else {
|
||||
completions.push(built);
|
||||
}
|
||||
if entry_name.eq(base)
|
||||
&& matches!(options.match_algorithm, MatchAlgorithm::Prefix)
|
||||
&& isdir
|
||||
{
|
||||
break;
|
||||
|
||||
// For https://github.com/nushell/nushell/issues/13204
|
||||
if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@ -147,15 +153,24 @@ fn surround_remove(partial: &str) -> String {
|
||||
partial.to_string()
|
||||
}
|
||||
|
||||
pub struct FileSuggestion {
|
||||
pub span: nu_protocol::Span,
|
||||
pub path: String,
|
||||
pub style: Option<Style>,
|
||||
}
|
||||
|
||||
/// # 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(
|
||||
want_directory: bool,
|
||||
span: nu_protocol::Span,
|
||||
partial: &str,
|
||||
cwd: &str,
|
||||
cwds: &[impl AsRef<str>],
|
||||
options: &CompletionOptions,
|
||||
engine_state: &EngineState,
|
||||
stack: &Stack,
|
||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
||||
) -> Vec<FileSuggestion> {
|
||||
let cleaned_partial = surround_remove(partial);
|
||||
let isdir = cleaned_partial.ends_with(is_separator);
|
||||
let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
|
||||
@ -175,18 +190,21 @@ pub fn complete_item(
|
||||
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
|
||||
&& engine_state.config.use_ansi_coloring)
|
||||
.then(|| {
|
||||
let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
|
||||
Some(v) => env_to_string("LS_COLORS", v, engine_state, stack).ok(),
|
||||
None => None,
|
||||
};
|
||||
get_ls_colors(ls_colors_env_str)
|
||||
});
|
||||
&& engine_state.config.use_ansi_coloring.get(engine_state))
|
||||
.then(|| {
|
||||
let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
|
||||
Some(v) => env_to_string("LS_COLORS", v, engine_state, stack).ok(),
|
||||
None => None,
|
||||
};
|
||||
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 original_cwd = OriginalCwd::None;
|
||||
|
||||
@ -194,19 +212,21 @@ pub fn complete_item(
|
||||
match components.peek().cloned() {
|
||||
Some(c @ Component::Prefix(..)) => {
|
||||
// windows only by definition
|
||||
cwd = [c, Component::RootDir].iter().collect();
|
||||
cwds = vec![[c, Component::RootDir].iter().collect()];
|
||||
prefix_len = c.as_os_str().len();
|
||||
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
|
||||
}
|
||||
Some(c @ Component::RootDir) => {
|
||||
// This is kind of a hack. When joining an empty string with the rest,
|
||||
// we add the slash automagically
|
||||
cwd = PathBuf::from(c.as_os_str());
|
||||
cwds = vec![PathBuf::from(c.as_os_str())];
|
||||
prefix_len = 1;
|
||||
original_cwd = OriginalCwd::Prefix(String::new());
|
||||
}
|
||||
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;
|
||||
original_cwd = OriginalCwd::Home;
|
||||
}
|
||||
@ -223,8 +243,14 @@ pub fn complete_item(
|
||||
|
||||
complete_rec(
|
||||
partial.as_slice(),
|
||||
&PathBuiltFromString::default(),
|
||||
&cwd,
|
||||
&cwds
|
||||
.into_iter()
|
||||
.map(|cwd| PathBuiltFromString {
|
||||
cwd,
|
||||
parts: Vec::new(),
|
||||
isdir: false,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
@ -245,7 +271,11 @@ pub fn complete_item(
|
||||
.map(lscolors::Style::to_nu_ansi_term_style)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
(span, escape_path(path, want_directory), style)
|
||||
FileSuggestion {
|
||||
span,
|
||||
path: escape_path(path, want_directory),
|
||||
style,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@ -253,8 +283,9 @@ pub fn complete_item(
|
||||
// Fix files or folders with quotes or hashes
|
||||
pub fn escape_path(path: String, dir: bool) -> String {
|
||||
// make glob pattern have the highest priority.
|
||||
let glob_contaminated = path.contains(['[', '*', ']', '?']);
|
||||
if glob_contaminated {
|
||||
if nu_glob::is_glob(path.as_str()) {
|
||||
let pathbuf = nu_path::expand_tilde(path);
|
||||
let path = pathbuf.to_string_lossy();
|
||||
return if path.contains('\'') {
|
||||
// decide to use double quote, also need to escape `"` in path
|
||||
// or else users can't do anything with completed path either.
|
||||
@ -310,45 +341,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.
|
||||
///
|
||||
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
|
||||
@ -359,6 +351,7 @@ fn collapse_ndots(path: PathBuiltFromString) -> PathBuiltFromString {
|
||||
let mut result = PathBuiltFromString {
|
||||
parts: Vec::with_capacity(path.parts.len()),
|
||||
isdir: path.isdir,
|
||||
cwd: path.cwd,
|
||||
};
|
||||
|
||||
let mut dot_count = 0;
|
||||
|
@ -1,7 +1,13 @@
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
use nu_parser::trim_quotes_str;
|
||||
use nu_protocol::{CompletionAlgorithm, CompletionSort};
|
||||
use std::fmt::Display;
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
use nucleo_matcher::{
|
||||
pattern::{Atom, AtomKind, CaseMatching, Normalization},
|
||||
Config, Matcher, Utf32Str,
|
||||
};
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
use super::SemanticSuggestion;
|
||||
|
||||
/// Describes how suggestions should be matched.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
@ -19,33 +25,170 @@ pub enum MatchAlgorithm {
|
||||
Fuzzy,
|
||||
}
|
||||
|
||||
impl MatchAlgorithm {
|
||||
/// Returns whether the `needle` search text matches the given `haystack`.
|
||||
pub fn matches_str(&self, haystack: &str, needle: &str) -> bool {
|
||||
let haystack = trim_quotes_str(haystack);
|
||||
let needle = trim_quotes_str(needle);
|
||||
match *self {
|
||||
MatchAlgorithm::Prefix => haystack.starts_with(needle),
|
||||
pub struct NuMatcher<T> {
|
||||
options: CompletionOptions,
|
||||
needle: String,
|
||||
state: State<T>,
|
||||
}
|
||||
|
||||
enum State<T> {
|
||||
Prefix {
|
||||
/// Holds (haystack, item)
|
||||
items: Vec<(String, T)>,
|
||||
},
|
||||
Fuzzy {
|
||||
matcher: Matcher,
|
||||
atom: Atom,
|
||||
/// Holds (haystack, item, score)
|
||||
items: Vec<(String, T, u16)>,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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 needle = trim_quotes_str(needle.as_ref());
|
||||
match options.match_algorithm {
|
||||
MatchAlgorithm::Prefix => {
|
||||
let lowercase_needle = if options.case_sensitive {
|
||||
needle.to_owned()
|
||||
} else {
|
||||
needle.to_folded_case()
|
||||
};
|
||||
NuMatcher {
|
||||
options,
|
||||
needle: lowercase_needle,
|
||||
state: State::Prefix { items: Vec::new() },
|
||||
}
|
||||
}
|
||||
MatchAlgorithm::Fuzzy => {
|
||||
let matcher = SkimMatcherV2::default();
|
||||
matcher.fuzzy_match(haystack, needle).is_some()
|
||||
let atom = Atom::new(
|
||||
needle,
|
||||
if options.case_sensitive {
|
||||
CaseMatching::Respect
|
||||
} else {
|
||||
CaseMatching::Ignore
|
||||
},
|
||||
Normalization::Smart,
|
||||
AtomKind::Fuzzy,
|
||||
false,
|
||||
);
|
||||
NuMatcher {
|
||||
options,
|
||||
needle: needle.to_owned(),
|
||||
state: State::Fuzzy {
|
||||
matcher: Matcher::new(Config::DEFAULT),
|
||||
atom,
|
||||
items: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the `needle` search text matches the given `haystack`.
|
||||
pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool {
|
||||
match *self {
|
||||
MatchAlgorithm::Prefix => haystack.starts_with(needle),
|
||||
MatchAlgorithm::Fuzzy => {
|
||||
let haystack_str = String::from_utf8_lossy(haystack);
|
||||
let needle_str = String::from_utf8_lossy(needle);
|
||||
|
||||
let matcher = SkimMatcherV2::default();
|
||||
matcher.fuzzy_match(&haystack_str, &needle_str).is_some()
|
||||
/// 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);
|
||||
match &mut self.state {
|
||||
State::Prefix { items } => {
|
||||
let haystack_folded = if self.options.case_sensitive {
|
||||
Cow::Borrowed(haystack)
|
||||
} else {
|
||||
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 {
|
||||
matcher,
|
||||
atom,
|
||||
items,
|
||||
} => {
|
||||
let mut haystack_buf = Vec::new();
|
||||
let haystack_utf32 = Utf32Str::new(trim_quotes_str(haystack), &mut haystack_buf);
|
||||
let mut indices = Vec::new();
|
||||
let Some(score) = atom.indices(haystack_utf32, matcher, &mut indices) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(item) = item {
|
||||
items.push((haystack.to_string(), item, score));
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the given item if the given haystack matches the needle.
|
||||
///
|
||||
/// Returns whether the item was added.
|
||||
pub fn add(&mut self, haystack: impl AsRef<str>, item: T) -> bool {
|
||||
self.matches_aux(haystack.as_ref(), Some(item))
|
||||
}
|
||||
|
||||
/// Returns whether the haystack matches the needle.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CompletionAlgorithm> for MatchAlgorithm {
|
||||
@ -105,35 +248,67 @@ impl Default for CompletionOptions {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::MatchAlgorithm;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn match_algorithm_prefix() {
|
||||
let algorithm = MatchAlgorithm::Prefix;
|
||||
use super::{CompletionOptions, MatchAlgorithm, NuMatcher};
|
||||
|
||||
assert!(algorithm.matches_str("example text", ""));
|
||||
assert!(algorithm.matches_str("example text", "examp"));
|
||||
assert!(!algorithm.matches_str("example text", "text"));
|
||||
|
||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
|
||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
|
||||
assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
|
||||
#[rstest]
|
||||
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
||||
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
||||
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
|
||||
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
|
||||
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
||||
#[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]
|
||||
fn match_algorithm_fuzzy() {
|
||||
let algorithm = MatchAlgorithm::Fuzzy;
|
||||
fn match_algorithm_fuzzy_sort_score() {
|
||||
let options = CompletionOptions {
|
||||
match_algorithm: MatchAlgorithm::Fuzzy,
|
||||
..Default::default()
|
||||
};
|
||||
let mut matcher = NuMatcher::new("fob", options);
|
||||
for item in ["foo/bar", "fob", "foo bar"] {
|
||||
matcher.add(item, item);
|
||||
}
|
||||
// Sort by score, then in alphabetical order
|
||||
assert_eq!(vec!["fob", "foo bar", "foo/bar"], matcher.results());
|
||||
}
|
||||
|
||||
assert!(algorithm.matches_str("example text", ""));
|
||||
assert!(algorithm.matches_str("example text", "examp"));
|
||||
assert!(algorithm.matches_str("example text", "ext"));
|
||||
assert!(algorithm.matches_str("example text", "mplxt"));
|
||||
assert!(!algorithm.matches_str("example text", "mpp"));
|
||||
|
||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
|
||||
assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
|
||||
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]));
|
||||
#[test]
|
||||
fn match_algorithm_fuzzy_sort_strip() {
|
||||
let options = CompletionOptions {
|
||||
match_algorithm: MatchAlgorithm::Fuzzy,
|
||||
..Default::default()
|
||||
};
|
||||
let mut matcher = NuMatcher::new("'love spaces' ", options);
|
||||
for item in [
|
||||
"'i love spaces'",
|
||||
"'i love spaces' so much",
|
||||
"'lovespaces' ",
|
||||
] {
|
||||
matcher.add(item, item);
|
||||
}
|
||||
// Make sure the spaces are respected
|
||||
assert_eq!(vec!["'i love spaces' so much"], matcher.results());
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,45 @@
|
||||
use crate::completions::{
|
||||
completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm,
|
||||
SemanticSuggestion,
|
||||
completer::map_value_completions, Completer, CompletionOptions, SemanticSuggestion,
|
||||
};
|
||||
use nu_engine::eval_call;
|
||||
use nu_protocol::{
|
||||
ast::{Argument, Call, Expr, Expression},
|
||||
debugger::WithoutDebug,
|
||||
engine::{Stack, StateWorkingSet},
|
||||
CompletionSort, DeclId, PipelineData, Span, Type, Value,
|
||||
DeclId, PipelineData, Span, Type, Value,
|
||||
};
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::completion_common::sort_suggestions;
|
||||
use super::completion_options::NuMatcher;
|
||||
|
||||
pub struct CustomCompletion {
|
||||
pub struct CustomCompletion<T: Completer> {
|
||||
stack: Stack,
|
||||
decl_id: DeclId,
|
||||
line: String,
|
||||
fallback: T,
|
||||
}
|
||||
|
||||
impl CustomCompletion {
|
||||
pub fn new(stack: Stack, decl_id: DeclId, line: String) -> Self {
|
||||
impl<T: Completer> CustomCompletion<T> {
|
||||
pub fn new(stack: Stack, decl_id: DeclId, line: String, fallback: T) -> Self {
|
||||
Self {
|
||||
stack,
|
||||
decl_id,
|
||||
line,
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for CustomCompletion {
|
||||
impl<T: Completer> Completer for CustomCompletion<T> {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
_stack: &Stack,
|
||||
stack: &Stack,
|
||||
prefix: &[u8],
|
||||
span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
completion_options: &CompletionOptions,
|
||||
orig_options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
// Line position
|
||||
let line_pos = pos - offset;
|
||||
@ -68,12 +68,12 @@ impl Completer for CustomCompletion {
|
||||
PipelineData::empty(),
|
||||
);
|
||||
|
||||
let mut custom_completion_options = None;
|
||||
let mut completion_options = orig_options.clone();
|
||||
let mut should_sort = true;
|
||||
|
||||
// Parse result
|
||||
let suggestions = result
|
||||
.and_then(|data| data.into_value(span))
|
||||
.map(|value| match &value {
|
||||
let suggestions = match result.and_then(|data| data.into_value(span)) {
|
||||
Ok(value) => match &value {
|
||||
Value::Record { val, .. } => {
|
||||
let completions = val
|
||||
.get("completions")
|
||||
@ -86,78 +86,70 @@ impl Completer for CustomCompletion {
|
||||
let options = val.get("options");
|
||||
|
||||
if let Some(Value::Record { val: options, .. }) = &options {
|
||||
let should_sort = options
|
||||
.get("sort")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(false);
|
||||
if let Some(sort) = options.get("sort").and_then(|val| val.as_bool().ok()) {
|
||||
should_sort = sort;
|
||||
}
|
||||
|
||||
custom_completion_options = Some(CompletionOptions {
|
||||
case_sensitive: options
|
||||
.get("case_sensitive")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(true),
|
||||
positional: options
|
||||
.get("positional")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(true),
|
||||
match_algorithm: match options.get("completion_algorithm") {
|
||||
Some(option) => option
|
||||
.coerce_string()
|
||||
.ok()
|
||||
.and_then(|option| option.try_into().ok())
|
||||
.unwrap_or(MatchAlgorithm::Prefix),
|
||||
None => completion_options.match_algorithm,
|
||||
},
|
||||
sort: if should_sort {
|
||||
CompletionSort::Alphabetical
|
||||
} else {
|
||||
CompletionSort::Smart
|
||||
},
|
||||
});
|
||||
if let Some(case_sensitive) = options
|
||||
.get("case_sensitive")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
{
|
||||
completion_options.case_sensitive = case_sensitive;
|
||||
}
|
||||
if let Some(positional) =
|
||||
options.get("positional").and_then(|val| val.as_bool().ok())
|
||||
{
|
||||
completion_options.positional = positional;
|
||||
}
|
||||
if let Some(algorithm) = options
|
||||
.get("completion_algorithm")
|
||||
.and_then(|option| option.coerce_string().ok())
|
||||
.and_then(|option| option.try_into().ok())
|
||||
{
|
||||
completion_options.match_algorithm = algorithm;
|
||||
}
|
||||
}
|
||||
|
||||
completions
|
||||
}
|
||||
Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset),
|
||||
_ => vec![],
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let options = custom_completion_options
|
||||
.as_ref()
|
||||
.unwrap_or(completion_options);
|
||||
let suggestions = filter(prefix, suggestions, options);
|
||||
sort_suggestions(&String::from_utf8_lossy(prefix), suggestions, options)
|
||||
}
|
||||
}
|
||||
|
||||
fn filter(
|
||||
prefix: &[u8],
|
||||
items: Vec<SemanticSuggestion>,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
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 {
|
||||
value.contains(&prefix)
|
||||
}
|
||||
Value::Nothing { .. } => {
|
||||
return self.fallback.fetch(
|
||||
working_set,
|
||||
stack,
|
||||
prefix,
|
||||
span,
|
||||
offset,
|
||||
pos,
|
||||
orig_options,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Custom completer returned invalid value of type {}",
|
||||
value.get_type().to_string()
|
||||
);
|
||||
return vec![];
|
||||
}
|
||||
},
|
||||
MatchAlgorithm::Fuzzy => options
|
||||
.match_algorithm
|
||||
.matches_u8(it.suggestion.value.as_bytes(), prefix),
|
||||
})
|
||||
.collect()
|
||||
Err(e) => {
|
||||
log::error!("Error getting custom completions: {e}");
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), completion_options);
|
||||
|
||||
if should_sort {
|
||||
for sugg in suggestions {
|
||||
matcher.add_semantic_suggestion(sugg);
|
||||
}
|
||||
matcher.results()
|
||||
} else {
|
||||
suggestions
|
||||
.into_iter()
|
||||
.filter(|sugg| matcher.matches(&sugg.suggestion.value))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ use crate::completions::{
|
||||
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
||||
Completer, CompletionOptions,
|
||||
};
|
||||
use nu_ansi_term::Style;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
Span,
|
||||
@ -10,7 +9,7 @@ use nu_protocol::{
|
||||
use reedline::Suggestion;
|
||||
use std::path::Path;
|
||||
|
||||
use super::SemanticSuggestion;
|
||||
use super::{completion_common::FileSuggestion, SemanticSuggestion};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DirectoryCompletion {}
|
||||
@ -47,11 +46,11 @@ impl Completer for DirectoryCompletion {
|
||||
.into_iter()
|
||||
.map(move |x| SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: x.1,
|
||||
style: x.2,
|
||||
value: x.path,
|
||||
style: x.style,
|
||||
span: reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
start: x.span.start - offset,
|
||||
end: x.span.end - offset,
|
||||
},
|
||||
..Suggestion::default()
|
||||
},
|
||||
@ -92,6 +91,6 @@ pub fn directory_completion(
|
||||
options: &CompletionOptions,
|
||||
engine_state: &EngineState,
|
||||
stack: &Stack,
|
||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
||||
complete_item(true, span, partial, cwd, options, engine_state, stack)
|
||||
) -> Vec<FileSuggestion> {
|
||||
complete_item(true, span, partial, &[cwd], options, engine_state, stack)
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
use crate::completions::{file_path_completion, Completer, CompletionOptions};
|
||||
use nu_path::expand_tilde;
|
||||
use nu_protocol::{
|
||||
engine::{Stack, StateWorkingSet},
|
||||
Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
|
||||
use std::path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
|
||||
|
||||
use super::{completion_common::sort_suggestions, SemanticSuggestion};
|
||||
use super::{SemanticSuggestion, SuggestionKind};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DotNuCompletion {}
|
||||
@ -28,108 +29,106 @@ impl Completer for DotNuCompletion {
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let prefix_str = String::from_utf8_lossy(prefix).replace('`', "");
|
||||
let mut search_dirs: Vec<String> = vec![];
|
||||
let prefix_str = String::from_utf8_lossy(prefix);
|
||||
let start_with_backquote = prefix_str.starts_with('`');
|
||||
let end_with_backquote = prefix_str.ends_with('`');
|
||||
let prefix_str = prefix_str.replace('`', "");
|
||||
let mut search_dirs: Vec<PathBuf> = vec![];
|
||||
|
||||
// If prefix_str is only a word we want to search in the current dir
|
||||
let (base, partial) = prefix_str
|
||||
.rsplit_once(is_separator)
|
||||
.unwrap_or((".", &prefix_str));
|
||||
let base_dir = base.replace(is_separator, MAIN_SEPARATOR_STR);
|
||||
let mut partial = partial.to_string();
|
||||
// On windows, this standardizes paths to use \
|
||||
let mut is_current_folder = false;
|
||||
|
||||
// Fetch the lib dirs
|
||||
let lib_dirs: Vec<String> = if let Some(lib_dirs) = working_set.get_env_var("NU_LIB_DIRS") {
|
||||
lib_dirs
|
||||
.as_list()
|
||||
.into_iter()
|
||||
.flat_map(|it| {
|
||||
it.iter().map(|x| {
|
||||
x.to_path()
|
||||
.expect("internal error: failed to convert lib path")
|
||||
})
|
||||
})
|
||||
.map(|it| {
|
||||
it.into_os_string()
|
||||
.into_string()
|
||||
.expect("internal error: failed to convert OS path")
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let lib_dirs: Vec<PathBuf> = working_set
|
||||
.find_variable(b"$NU_LIB_DIRS")
|
||||
.and_then(|vid| working_set.get_variable(vid).const_val.as_ref())
|
||||
.or(working_set.get_env_var("NU_LIB_DIRS"))
|
||||
.map(|lib_dirs| {
|
||||
lib_dirs
|
||||
.as_list()
|
||||
.into_iter()
|
||||
.flat_map(|it| it.iter().filter_map(|x| x.to_path().ok()))
|
||||
.map(expand_tilde)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Check if the base_dir is a folder
|
||||
// rsplit_once removes the separator
|
||||
let cwd = working_set.permanent_state.cwd(None);
|
||||
if base_dir != "." {
|
||||
// Add the base dir into the directories to be searched
|
||||
search_dirs.push(base_dir.clone());
|
||||
|
||||
// Reset the partial adding the basic dir back
|
||||
// in order to make the span replace work properly
|
||||
let mut base_dir_partial = base_dir;
|
||||
base_dir_partial.push_str(&partial);
|
||||
|
||||
partial = base_dir_partial;
|
||||
// Search in base_dir as well as lib_dirs
|
||||
if let Ok(mut cwd) = cwd {
|
||||
cwd.push(&base_dir);
|
||||
search_dirs.push(cwd.into_std_path_buf());
|
||||
}
|
||||
search_dirs.extend(lib_dirs.into_iter().map(|mut dir| {
|
||||
dir.push(&base_dir);
|
||||
dir
|
||||
}));
|
||||
} else {
|
||||
// Fetch the current folder
|
||||
#[allow(deprecated)]
|
||||
let current_folder = working_set.permanent_state.current_work_dir();
|
||||
is_current_folder = true;
|
||||
|
||||
// Add the current folder and the lib dirs into the
|
||||
// directories to be searched
|
||||
search_dirs.push(current_folder);
|
||||
if let Ok(cwd) = cwd {
|
||||
search_dirs.push(cwd.into_std_path_buf());
|
||||
}
|
||||
search_dirs.extend(lib_dirs);
|
||||
}
|
||||
|
||||
// Fetch the files filtering the ones that ends with .nu
|
||||
// and transform them into suggestions
|
||||
let output: Vec<SemanticSuggestion> = search_dirs
|
||||
let completions = file_path_completion(
|
||||
span,
|
||||
partial,
|
||||
&search_dirs
|
||||
.iter()
|
||||
.filter_map(|d| d.to_str())
|
||||
.collect::<Vec<_>>(),
|
||||
options,
|
||||
working_set.permanent_state,
|
||||
stack,
|
||||
);
|
||||
completions
|
||||
.into_iter()
|
||||
.flat_map(|search_dir| {
|
||||
let completions = file_path_completion(
|
||||
span,
|
||||
&partial,
|
||||
&search_dir,
|
||||
options,
|
||||
working_set.permanent_state,
|
||||
stack,
|
||||
);
|
||||
completions
|
||||
.into_iter()
|
||||
.filter(move |it| {
|
||||
// Different base dir, so we list the .nu files or folders
|
||||
if !is_current_folder {
|
||||
it.1.ends_with(".nu") || it.1.ends_with(SEP)
|
||||
} else {
|
||||
// Lib dirs, so we filter only the .nu files or directory modules
|
||||
if it.1.ends_with(SEP) {
|
||||
Path::new(&search_dir).join(&it.1).join("mod.nu").exists()
|
||||
} else {
|
||||
it.1.ends_with(".nu")
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(move |x| SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: x.1,
|
||||
style: x.2,
|
||||
span: reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
append_whitespace: true,
|
||||
..Suggestion::default()
|
||||
},
|
||||
// TODO????
|
||||
kind: None,
|
||||
})
|
||||
// Different base dir, so we list the .nu files or folders
|
||||
.filter(|it| {
|
||||
// for paths with spaces in them
|
||||
let path = it.path.trim_end_matches('`');
|
||||
path.ends_with(".nu") || path.ends_with(SEP)
|
||||
})
|
||||
.collect();
|
||||
|
||||
sort_suggestions(&prefix_str, output, options)
|
||||
.map(|x| {
|
||||
let append_whitespace =
|
||||
x.path.ends_with(".nu") && (!start_with_backquote || end_with_backquote);
|
||||
// Re-calculate the span to replace
|
||||
let mut span_offset = 0;
|
||||
let mut value = x.path.to_string();
|
||||
// Complete only the last path component
|
||||
if base_dir != "." {
|
||||
span_offset = base_dir.len() + 1
|
||||
}
|
||||
// Retain only one '`'
|
||||
if start_with_backquote {
|
||||
value = value.trim_start_matches('`').to_string();
|
||||
span_offset += 1;
|
||||
}
|
||||
// Add the backquote back
|
||||
if end_with_backquote && !value.ends_with('`') {
|
||||
value.push('`');
|
||||
}
|
||||
let end = x.span.end - offset;
|
||||
let start = std::cmp::min(end, x.span.start - offset + span_offset);
|
||||
SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value,
|
||||
style: x.style,
|
||||
span: reedline::Span { start, end },
|
||||
append_whitespace,
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::Module),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,14 @@ use crate::completions::{
|
||||
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
|
||||
Completer, CompletionOptions,
|
||||
};
|
||||
use nu_ansi_term::Style;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
Span,
|
||||
};
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
use reedline::Suggestion;
|
||||
use std::path::Path;
|
||||
|
||||
use super::SemanticSuggestion;
|
||||
use super::{completion_common::FileSuggestion, SemanticSuggestion};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FileCompletion {}
|
||||
@ -44,7 +42,7 @@ impl Completer for FileCompletion {
|
||||
readjusted,
|
||||
span,
|
||||
&prefix,
|
||||
&working_set.permanent_state.current_work_dir(),
|
||||
&[&working_set.permanent_state.current_work_dir()],
|
||||
options,
|
||||
working_set.permanent_state,
|
||||
stack,
|
||||
@ -52,11 +50,11 @@ impl Completer for FileCompletion {
|
||||
.into_iter()
|
||||
.map(move |x| SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: x.1,
|
||||
style: x.2,
|
||||
value: x.path,
|
||||
style: x.style,
|
||||
span: reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
start: x.span.start - offset,
|
||||
end: x.span.end - offset,
|
||||
},
|
||||
..Suggestion::default()
|
||||
},
|
||||
@ -95,21 +93,10 @@ impl Completer for FileCompletion {
|
||||
pub fn file_path_completion(
|
||||
span: nu_protocol::Span,
|
||||
partial: &str,
|
||||
cwd: &str,
|
||||
cwds: &[impl AsRef<str>],
|
||||
options: &CompletionOptions,
|
||||
engine_state: &EngineState,
|
||||
stack: &Stack,
|
||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
||||
complete_item(false, span, partial, cwd, 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)
|
||||
) -> Vec<FileSuggestion> {
|
||||
complete_item(false, span, partial, cwds, options, engine_state, stack)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::completions::{completion_common::sort_suggestions, Completer, CompletionOptions};
|
||||
use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
|
||||
use nu_protocol::{
|
||||
ast::{Expr, Expression},
|
||||
engine::{Stack, StateWorkingSet},
|
||||
@ -35,7 +35,7 @@ impl Completer for FlagCompletion {
|
||||
let decl = working_set.get_decl(call.decl_id);
|
||||
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 {
|
||||
let flag_desc = &named.desc;
|
||||
@ -44,34 +44,7 @@ impl Completer for FlagCompletion {
|
||||
short.encode_utf8(&mut named);
|
||||
named.insert(0, b'-');
|
||||
|
||||
if options.match_algorithm.matches_u8(&named, prefix) {
|
||||
output.push(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: String::from_utf8_lossy(&named).to_string(),
|
||||
description: Some(flag_desc.to_string()),
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
append_whitespace: true,
|
||||
..Suggestion::default()
|
||||
},
|
||||
// TODO????
|
||||
kind: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if named.long.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut named = named.long.as_bytes().to_vec();
|
||||
named.insert(0, b'-');
|
||||
named.insert(0, b'-');
|
||||
|
||||
if options.match_algorithm.matches_u8(&named, prefix) {
|
||||
output.push(SemanticSuggestion {
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: String::from_utf8_lossy(&named).to_string(),
|
||||
description: Some(flag_desc.to_string()),
|
||||
@ -86,9 +59,32 @@ impl Completer for FlagCompletion {
|
||||
kind: None,
|
||||
});
|
||||
}
|
||||
|
||||
if named.long.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut named = named.long.as_bytes().to_vec();
|
||||
named.insert(0, b'-');
|
||||
named.insert(0, b'-');
|
||||
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: String::from_utf8_lossy(&named).to_string(),
|
||||
description: Some(flag_desc.to_string()),
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
append_whitespace: true,
|
||||
..Suggestion::default()
|
||||
},
|
||||
// TODO????
|
||||
kind: None,
|
||||
});
|
||||
}
|
||||
|
||||
return sort_suggestions(&String::from_utf8_lossy(prefix), output, options);
|
||||
return matcher.results();
|
||||
}
|
||||
|
||||
vec![]
|
||||
|
@ -18,7 +18,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
|
||||
pub use custom_completions::CustomCompletion;
|
||||
pub use directory_completions::DirectoryCompletion;
|
||||
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 operator_completions::OperatorCompletion;
|
||||
pub use variable_completions::VariableCompletion;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::completions::{
|
||||
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
|
||||
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||
};
|
||||
use nu_protocol::{
|
||||
ast::{Expr, Expression},
|
||||
@ -28,7 +28,7 @@ impl Completer for OperatorCompletion {
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_pos: usize,
|
||||
_options: &CompletionOptions,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
//Check if int, float, or string
|
||||
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"),
|
||||
("in", "Is 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![
|
||||
("=~", "Contains regex match"),
|
||||
("like", "Contains 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)"),
|
||||
("not-in", "Is not a member of (doesn't use regex)"),
|
||||
@ -93,10 +91,6 @@ impl Completer for OperatorCompletion {
|
||||
("**", "Power of"),
|
||||
("in", "Is 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![
|
||||
(
|
||||
@ -111,33 +105,28 @@ impl Completer for OperatorCompletion {
|
||||
("not", "Negates a value or expression"),
|
||||
("in", "Is 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::List(_) => vec![(
|
||||
"++",
|
||||
"Appends two lists, a list and a value, two strings, or two binary values",
|
||||
)],
|
||||
Expr::List(_) => vec![
|
||||
(
|
||||
"++",
|
||||
"Concatenates two lists, two strings, or two binary values",
|
||||
),
|
||||
("has", "Contains a value of (doesn't use regex)"),
|
||||
("not-has", "Does not contain a value of (doesn't use regex)"),
|
||||
],
|
||||
Expr::Var(id) => get_variable_completions(id, working_set),
|
||||
_ => vec![],
|
||||
},
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let match_algorithm = MatchAlgorithm::Prefix;
|
||||
let input_fuzzy_search =
|
||||
|(operator, _): &(&str, &str)| match_algorithm.matches_str(operator, partial);
|
||||
|
||||
possible_operations
|
||||
.into_iter()
|
||||
.filter(input_fuzzy_search)
|
||||
.map(move |x| SemanticSuggestion {
|
||||
let mut matcher = NuMatcher::new(partial, options.clone());
|
||||
for (symbol, desc) in possible_operations.into_iter() {
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: x.0.to_string(),
|
||||
description: Some(x.1.to_string()),
|
||||
value: symbol.to_string(),
|
||||
description: Some(desc.to_string()),
|
||||
span: reedline::Span::new(span.start - offset, span.end - offset),
|
||||
append_whitespace: true,
|
||||
..Suggestion::default()
|
||||
@ -145,8 +134,9 @@ impl Completer for OperatorCompletion {
|
||||
kind: Some(SuggestionKind::Command(
|
||||
nu_protocol::engine::CommandType::Builtin,
|
||||
)),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
}
|
||||
matcher.results()
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +153,7 @@ pub fn get_variable_completions<'a>(
|
||||
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."),
|
||||
],
|
||||
|
@ -1,6 +1,4 @@
|
||||
use crate::completions::{
|
||||
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
|
||||
};
|
||||
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
|
||||
use nu_engine::{column::get_columns, eval_variable};
|
||||
use nu_protocol::{
|
||||
engine::{Stack, StateWorkingSet},
|
||||
@ -9,7 +7,7 @@ use nu_protocol::{
|
||||
use reedline::Suggestion;
|
||||
use std::str;
|
||||
|
||||
use super::completion_common::sort_suggestions;
|
||||
use super::completion_options::NuMatcher;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VariableCompletion {
|
||||
@ -33,7 +31,6 @@ impl Completer for VariableCompletion {
|
||||
_pos: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let mut output = vec![];
|
||||
let builtins = ["$nu", "$in", "$env"];
|
||||
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
|
||||
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 prefix_str = String::from_utf8_lossy(prefix);
|
||||
let mut matcher = NuMatcher::new(prefix_str, options.clone());
|
||||
|
||||
// Completions for the given variable
|
||||
if !var_str.is_empty() {
|
||||
@ -63,37 +61,25 @@ impl Completer for VariableCompletion {
|
||||
|
||||
if let Some(val) = env_vars.get(&target_var_str) {
|
||||
for suggestion in nested_suggestions(val, &nested_levels, current_span) {
|
||||
if options.match_algorithm.matches_u8_insensitive(
|
||||
options.case_sensitive,
|
||||
suggestion.suggestion.value.as_bytes(),
|
||||
prefix,
|
||||
) {
|
||||
output.push(suggestion);
|
||||
}
|
||||
matcher.add_semantic_suggestion(suggestion);
|
||||
}
|
||||
|
||||
return sort_suggestions(&prefix_str, output, options);
|
||||
return matcher.results();
|
||||
}
|
||||
} else {
|
||||
// No nesting provided, return all env vars
|
||||
for env_var in env_vars {
|
||||
if options.match_algorithm.matches_u8_insensitive(
|
||||
options.case_sensitive,
|
||||
env_var.0.as_bytes(),
|
||||
prefix,
|
||||
) {
|
||||
output.push(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: env_var.0,
|
||||
span: current_span,
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
|
||||
});
|
||||
}
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: env_var.0,
|
||||
span: current_span,
|
||||
..Suggestion::default()
|
||||
},
|
||||
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)
|
||||
{
|
||||
if options.match_algorithm.matches_u8_insensitive(
|
||||
options.case_sensitive,
|
||||
suggestion.suggestion.value.as_bytes(),
|
||||
prefix,
|
||||
) {
|
||||
output.push(suggestion);
|
||||
}
|
||||
matcher.add_semantic_suggestion(suggestion);
|
||||
}
|
||||
|
||||
return sort_suggestions(&prefix_str, output, options);
|
||||
return matcher.results();
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,37 +110,25 @@ impl Completer for VariableCompletion {
|
||||
if let Ok(value) = var {
|
||||
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span)
|
||||
{
|
||||
if options.match_algorithm.matches_u8_insensitive(
|
||||
options.case_sensitive,
|
||||
suggestion.suggestion.value.as_bytes(),
|
||||
prefix,
|
||||
) {
|
||||
output.push(suggestion);
|
||||
}
|
||||
matcher.add_semantic_suggestion(suggestion);
|
||||
}
|
||||
|
||||
return sort_suggestions(&prefix_str, output, options);
|
||||
return matcher.results();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Variable completion (e.g: $en<tab> to complete $env)
|
||||
for builtin in builtins {
|
||||
if options.match_algorithm.matches_u8_insensitive(
|
||||
options.case_sensitive,
|
||||
builtin.as_bytes(),
|
||||
prefix,
|
||||
) {
|
||||
output.push(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: builtin.to_string(),
|
||||
span: current_span,
|
||||
..Suggestion::default()
|
||||
},
|
||||
// TODO is there a way to get the VarId to get the type???
|
||||
kind: None,
|
||||
});
|
||||
}
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: builtin.to_string(),
|
||||
span: current_span,
|
||||
..Suggestion::default()
|
||||
},
|
||||
// TODO is there a way to get the VarId to get the type???
|
||||
kind: None,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: The following can be refactored (see find_commands_by_predicate() used in
|
||||
@ -170,40 +138,7 @@ impl Completer for VariableCompletion {
|
||||
for scope_frame in working_set.delta.scope.iter().rev() {
|
||||
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
|
||||
for v in &overlay_frame.vars {
|
||||
if options.match_algorithm.matches_u8_insensitive(
|
||||
options.case_sensitive,
|
||||
v.0,
|
||||
prefix,
|
||||
) {
|
||||
output.push(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: String::from_utf8_lossy(v.0).to_string(),
|
||||
span: current_span,
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::Type(
|
||||
working_set.get_variable(*v.1).ty.clone(),
|
||||
)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Permanent state vars
|
||||
// for scope in &self.engine_state.scope {
|
||||
for overlay_frame in working_set
|
||||
.permanent_state
|
||||
.active_overlays(&removed_overlays)
|
||||
.rev()
|
||||
{
|
||||
for v in &overlay_frame.vars {
|
||||
if options.match_algorithm.matches_u8_insensitive(
|
||||
options.case_sensitive,
|
||||
v.0,
|
||||
prefix,
|
||||
) {
|
||||
output.push(SemanticSuggestion {
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: String::from_utf8_lossy(v.0).to_string(),
|
||||
span: current_span,
|
||||
@ -217,11 +152,28 @@ impl Completer for VariableCompletion {
|
||||
}
|
||||
}
|
||||
|
||||
output = sort_suggestions(&prefix_str, output, options);
|
||||
// Permanent state vars
|
||||
// for scope in &self.engine_state.scope {
|
||||
for overlay_frame in working_set
|
||||
.permanent_state
|
||||
.active_overlays(&removed_overlays)
|
||||
.rev()
|
||||
{
|
||||
for v in &overlay_frame.vars {
|
||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||
suggestion: Suggestion {
|
||||
value: String::from_utf8_lossy(v.0).to_string(),
|
||||
span: current_span,
|
||||
..Suggestion::default()
|
||||
},
|
||||
kind: Some(SuggestionKind::Type(
|
||||
working_set.get_variable(*v.1).ty.clone(),
|
||||
)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
output.dedup(); // TODO: Removes only consecutive duplicates, is it intended?
|
||||
|
||||
output
|
||||
matcher.results()
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,13 +254,3 @@ fn recursive_value(val: &Value, sublevels: &[Vec<u8>]) -> Result<Value, Span> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ const OLD_PLUGIN_FILE: &str = "plugin.nu";
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) {
|
||||
use nu_protocol::ShellError;
|
||||
use nu_protocol::{shell_error::io::IoError, ShellError};
|
||||
use std::path::Path;
|
||||
|
||||
let span = plugin_file.as_ref().map(|s| s.span);
|
||||
@ -49,7 +49,10 @@ pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Span
|
||||
perf!(
|
||||
"add plugin file to engine_state",
|
||||
start_time,
|
||||
engine_state.get_config().use_ansi_coloring
|
||||
engine_state
|
||||
.get_config()
|
||||
.use_ansi_coloring
|
||||
.get(engine_state)
|
||||
);
|
||||
|
||||
start_time = std::time::Instant::now();
|
||||
@ -75,16 +78,12 @@ pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Span
|
||||
} else {
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: format!(
|
||||
"Error while opening plugin registry file: {}",
|
||||
plugin_path.display()
|
||||
),
|
||||
msg: "plugin path defined here".into(),
|
||||
span,
|
||||
help: None,
|
||||
inner: vec![err.into()],
|
||||
},
|
||||
&ShellError::Io(IoError::new_internal_with_path(
|
||||
err.kind(),
|
||||
"Could not open plugin registry file",
|
||||
nu_protocol::location!(),
|
||||
plugin_path,
|
||||
)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -129,7 +128,10 @@ pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Span
|
||||
perf!(
|
||||
&format!("read plugin file {}", plugin_path.display()),
|
||||
start_time,
|
||||
engine_state.get_config().use_ansi_coloring
|
||||
engine_state
|
||||
.get_config()
|
||||
.use_ansi_coloring
|
||||
.get(engine_state)
|
||||
);
|
||||
start_time = std::time::Instant::now();
|
||||
|
||||
@ -145,7 +147,10 @@ pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Span
|
||||
perf!(
|
||||
&format!("load plugin file {}", plugin_path.display()),
|
||||
start_time,
|
||||
engine_state.get_config().use_ansi_coloring
|
||||
engine_state
|
||||
.get_config()
|
||||
.use_ansi_coloring
|
||||
.get(engine_state)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -225,8 +230,8 @@ pub fn eval_config_contents(
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool {
|
||||
use nu_protocol::{
|
||||
PluginExample, PluginIdentity, PluginRegistryItem, PluginRegistryItemData, PluginSignature,
|
||||
ShellError,
|
||||
shell_error::io::IoError, PluginExample, PluginIdentity, PluginRegistryItem,
|
||||
PluginRegistryItemData, PluginSignature, ShellError,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@ -315,7 +320,15 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool {
|
||||
// Write the new file
|
||||
let new_plugin_file_path = config_dir.join(PLUGIN_FILE);
|
||||
if let Err(err) = std::fs::File::create(&new_plugin_file_path)
|
||||
.map_err(|e| e.into())
|
||||
.map_err(|err| {
|
||||
IoError::new_internal_with_path(
|
||||
err.kind(),
|
||||
"Could not create new plugin file",
|
||||
nu_protocol::location!(),
|
||||
new_plugin_file_path.clone(),
|
||||
)
|
||||
})
|
||||
.map_err(ShellError::from)
|
||||
.and_then(|file| contents.write_to(file, None))
|
||||
{
|
||||
report_shell_error(
|
||||
@ -345,7 +358,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool {
|
||||
perf!(
|
||||
"migrate old plugin file",
|
||||
start_time,
|
||||
engine_state.get_config().use_ansi_coloring
|
||||
engine_state
|
||||
.get_config()
|
||||
.use_ansi_coloring
|
||||
.get(&engine_state)
|
||||
);
|
||||
true
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use log::info;
|
||||
use nu_engine::{convert_env_values, eval_block};
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
use nu_protocol::{
|
||||
cli_error::report_compile_error,
|
||||
@ -9,6 +9,8 @@ use nu_protocol::{
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::util::print_pipeline;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EvaluateCommandsOpts {
|
||||
pub table_mode: Option<Value>,
|
||||
@ -48,9 +50,6 @@ pub fn evaluate_commands(
|
||||
}
|
||||
}
|
||||
|
||||
// Translate environment variables from Strings to Values
|
||||
convert_env_values(engine_state, stack)?;
|
||||
|
||||
// Parse the source code
|
||||
let (block, delta) = {
|
||||
if let Some(ref t_mode) = table_mode {
|
||||
@ -72,7 +71,7 @@ pub fn evaluate_commands(
|
||||
|
||||
if let Some(err) = working_set.compile_errors.first() {
|
||||
report_compile_error(&working_set, err);
|
||||
// Not a fatal error, for now
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
(output, working_set.render())
|
||||
@ -93,7 +92,7 @@ pub fn evaluate_commands(
|
||||
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!());
|
||||
|
||||
|
@ -1,15 +1,17 @@
|
||||
use crate::util::eval_source;
|
||||
use crate::util::{eval_source, print_pipeline};
|
||||
use log::{info, trace};
|
||||
use nu_engine::{convert_env_values, eval_block};
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
use nu_path::canonicalize_with;
|
||||
use nu_protocol::{
|
||||
cli_error::report_compile_error,
|
||||
debugger::WithoutDebug,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
report_parse_error, report_parse_warning, PipelineData, ShellError, Span, Value,
|
||||
report_parse_error, report_parse_warning,
|
||||
shell_error::io::IoError,
|
||||
PipelineData, ShellError, Span, Value,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
/// Entry point for evaluating a file.
|
||||
///
|
||||
@ -22,16 +24,16 @@ pub fn evaluate_file(
|
||||
stack: &mut Stack,
|
||||
input: PipelineData,
|
||||
) -> Result<(), ShellError> {
|
||||
// Convert environment variables from Strings to Values and store them in the engine state.
|
||||
convert_env_values(engine_state, stack)?;
|
||||
|
||||
let cwd = engine_state.cwd_as_string(Some(stack))?;
|
||||
|
||||
let file_path =
|
||||
canonicalize_with(&path, cwd).map_err(|err| ShellError::FileNotFoundCustom {
|
||||
msg: format!("Could not access file '{path}': {err}"),
|
||||
span: Span::unknown(),
|
||||
})?;
|
||||
let file_path = canonicalize_with(&path, cwd).map_err(|err| {
|
||||
IoError::new_with_additional_context(
|
||||
err.kind(),
|
||||
Span::unknown(),
|
||||
PathBuf::from(&path),
|
||||
"Could not access file",
|
||||
)
|
||||
})?;
|
||||
|
||||
let file_path_str = file_path
|
||||
.to_str()
|
||||
@ -43,18 +45,24 @@ pub fn evaluate_file(
|
||||
span: Span::unknown(),
|
||||
})?;
|
||||
|
||||
let file = std::fs::read(&file_path).map_err(|err| ShellError::FileNotFoundCustom {
|
||||
msg: format!("Could not read file '{file_path_str}': {err}"),
|
||||
span: Span::unknown(),
|
||||
let file = std::fs::read(&file_path).map_err(|err| {
|
||||
IoError::new_with_additional_context(
|
||||
err.kind(),
|
||||
Span::unknown(),
|
||||
file_path.clone(),
|
||||
"Could not read file",
|
||||
)
|
||||
})?;
|
||||
engine_state.file = Some(file_path.clone());
|
||||
|
||||
let parent = file_path
|
||||
.parent()
|
||||
.ok_or_else(|| ShellError::FileNotFoundCustom {
|
||||
msg: format!("The file path '{file_path_str}' does not have a parent"),
|
||||
span: Span::unknown(),
|
||||
})?;
|
||||
let parent = file_path.parent().ok_or_else(|| {
|
||||
IoError::new_with_additional_context(
|
||||
std::io::ErrorKind::NotFound,
|
||||
Span::unknown(),
|
||||
file_path.clone(),
|
||||
"The file path does not have a parent",
|
||||
)
|
||||
})?;
|
||||
|
||||
stack.add_env_var(
|
||||
"FILE_PWD".to_string(),
|
||||
@ -89,7 +97,7 @@ pub fn evaluate_file(
|
||||
|
||||
if let Some(err) = working_set.compile_errors.first() {
|
||||
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.
|
||||
@ -119,7 +127,7 @@ pub fn evaluate_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.
|
||||
// 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()
|
||||
.print_raw(engine_state, no_newline, to_stderr)?;
|
||||
} else {
|
||||
arg.into_pipeline_data()
|
||||
.print(engine_state, stack, no_newline, to_stderr)?;
|
||||
arg.into_pipeline_data().print_table(
|
||||
engine_state,
|
||||
stack,
|
||||
no_newline,
|
||||
to_stderr,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
input.print_raw(engine_state, no_newline, to_stderr)?;
|
||||
} 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 log::trace;
|
||||
use log::{trace, warn};
|
||||
use nu_engine::ClosureEvalOnce;
|
||||
use nu_protocol::{
|
||||
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 POST_PROMPT_MARKER: &str = "\x1b]133;B\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;";
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
|
||||
|
||||
// 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_POST_PROMPT_MARKER: &str = "\x1b]633;B\x1b\\";
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
|
||||
#[allow(dead_code)]
|
||||
//"\x1b]633;D;{}\x1b\\"
|
||||
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\\";
|
||||
#[allow(dead_code)]
|
||||
//"\x1b]633;E;{}\x1b\\"
|
||||
pub(crate) const VSCODE_COMMANDLINE_MARKER_PREFIX: &str = "\x1b]633;E;";
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const VSCODE_COMMANDLINE_MARKER_SUFFIX: &str = "\x1b\\";
|
||||
#[allow(dead_code)]
|
||||
// "\x1b]633;P;Cwd={}\x1b\\"
|
||||
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 RESET_APPLICATION_MODE: &str = "\x1b[?1l";
|
||||
@ -89,18 +80,19 @@ fn get_prompt_string(
|
||||
})
|
||||
.and_then(|pipeline_data| {
|
||||
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.
|
||||
if x.ends_with('\n') {
|
||||
x.pop();
|
||||
}
|
||||
|
||||
if x.ends_with('\r') {
|
||||
x.pop();
|
||||
}
|
||||
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..]
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|num| (1..=20).contains(num))
|
||||
.filter(|num| (1..=35).contains(num))
|
||||
.map(KeyCode::F)
|
||||
.ok_or(ShellError::InvalidValue {
|
||||
valid: "'f1', 'f2', ..., or 'f20'".into(),
|
||||
valid: "'f1', 'f2', ..., or 'f35'".into(),
|
||||
actual: format!("'{keycode}'"),
|
||||
span: keybinding.keycode.span(),
|
||||
})?,
|
||||
|
@ -16,11 +16,12 @@ use crate::{
|
||||
use crossterm::cursor::SetCursorStyle;
|
||||
use log::{error, trace, warn};
|
||||
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;
|
||||
#[allow(deprecated)]
|
||||
use nu_engine::{convert_env_values, current_dir_str, env_to_strings};
|
||||
use nu_engine::env_to_strings;
|
||||
use nu_parser::{lex, parse, trim_quotes_str};
|
||||
use nu_protocol::shell_error::io::IoError;
|
||||
use nu_protocol::{
|
||||
config::NuCursorShape,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
@ -61,9 +62,7 @@ pub fn evaluate_repl(
|
||||
// from the Arc. This lets us avoid copying stack variables needlessly
|
||||
let mut unique_stack = stack.clone();
|
||||
let config = engine_state.get_config();
|
||||
let use_color = config.use_ansi_coloring;
|
||||
|
||||
confirm_stdin_is_terminal()?;
|
||||
let use_color = config.use_ansi_coloring.get(engine_state);
|
||||
|
||||
let mut entry_num = 0;
|
||||
|
||||
@ -81,13 +80,6 @@ pub fn evaluate_repl(
|
||||
stack.clone(),
|
||||
);
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
// Translate environment variables from Strings to Values
|
||||
if let Err(e) = convert_env_values(engine_state, &unique_stack) {
|
||||
report_shell_error(engine_state, &e);
|
||||
}
|
||||
perf!("translate env vars", start_time, use_color);
|
||||
|
||||
// seed env vars
|
||||
unique_stack.add_env_var(
|
||||
"CMD_DURATION_MS".into(),
|
||||
@ -111,6 +103,8 @@ pub fn evaluate_repl(
|
||||
engine_state.merge_env(&mut unique_stack)?;
|
||||
}
|
||||
|
||||
confirm_stdin_is_terminal()?;
|
||||
|
||||
let hostname = System::host_name();
|
||||
if shell_integration_osc2 {
|
||||
run_shell_integration_osc2(None, engine_state, &mut unique_stack, use_color);
|
||||
@ -130,13 +124,8 @@ pub fn evaluate_repl(
|
||||
// escape a few things because this says so
|
||||
// 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 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) {
|
||||
replaced_cmd_text.push(c);
|
||||
}
|
||||
let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?;
|
||||
|
||||
run_shell_integration_osc633(
|
||||
engine_state,
|
||||
@ -151,15 +140,30 @@ pub fn evaluate_repl(
|
||||
// Regenerate the $nu constant to contain the startup time and any other potential updates
|
||||
engine_state.generate_nu_constant();
|
||||
|
||||
if load_std_lib.is_none() && engine_state.get_config().show_banner {
|
||||
eval_source(
|
||||
engine_state,
|
||||
&mut unique_stack,
|
||||
r#"banner"#.as_bytes(),
|
||||
"show_banner",
|
||||
PipelineData::empty(),
|
||||
false,
|
||||
);
|
||||
if load_std_lib.is_none() {
|
||||
match engine_state.get_config().show_banner {
|
||||
Value::Bool { val: false, .. } => {}
|
||||
Value::String { ref val, .. } if val == "short" => {
|
||||
eval_source(
|
||||
engine_state,
|
||||
&mut unique_stack,
|
||||
r#"banner --short"#.as_bytes(),
|
||||
"show short banner",
|
||||
PipelineData::empty(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
eval_source(
|
||||
engine_state,
|
||||
&mut unique_stack,
|
||||
r#"banner"#.as_bytes(),
|
||||
"show_banner",
|
||||
PipelineData::empty(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kitty_protocol_healthcheck(engine_state);
|
||||
@ -220,26 +224,41 @@ pub fn evaluate_repl(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unescape_for_vscode(text: &mut std::str::Chars) -> Option<char> {
|
||||
match text.next() {
|
||||
Some('\\') => match text.next() {
|
||||
Some('0') => Some('\x00'), // NUL '\0' (null character)
|
||||
Some('a') => Some('\x07'), // BEL '\a' (bell)
|
||||
Some('b') => Some('\x08'), // BS '\b' (backspace)
|
||||
Some('t') => Some('\x09'), // HT '\t' (horizontal tab)
|
||||
Some('n') => Some('\x0a'), // LF '\n' (new line)
|
||||
Some('v') => Some('\x0b'), // VT '\v' (vertical tab)
|
||||
Some('f') => Some('\x0c'), // FF '\f' (form feed)
|
||||
Some('r') => Some('\x0d'), // CR '\r' (carriage ret)
|
||||
Some(';') => Some('\x3b'), // semi-colon
|
||||
Some('\\') => Some('\x5c'), // backslash
|
||||
Some('e') => Some('\x1b'), // escape
|
||||
Some(c) => Some(c),
|
||||
None => None,
|
||||
},
|
||||
Some(c) => Some(c),
|
||||
None => None,
|
||||
}
|
||||
fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
|
||||
let bytes = input
|
||||
.chars()
|
||||
.flat_map(|c| {
|
||||
let mut buf = [0; 4]; // Buffer to hold UTF-8 bytes of the character
|
||||
let c_bytes = c.encode_utf8(&mut buf); // Get UTF-8 bytes for the character
|
||||
|
||||
if c_bytes.len() == 1 {
|
||||
let byte = c_bytes.as_bytes()[0];
|
||||
|
||||
match byte {
|
||||
// Escape bytes below 0x20
|
||||
b if b < 0x20 => format!("\\x{:02X}", byte).into_bytes(),
|
||||
// Escape semicolon as \x3B
|
||||
b';' => "\\x3B".to_string().into_bytes(),
|
||||
// Escape backslash as \\
|
||||
b'\\' => "\\\\".to_string().into_bytes(),
|
||||
// Otherwise, return the character unchanged
|
||||
_ => 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> {
|
||||
@ -296,9 +315,6 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
if let Err(err) = engine_state.merge_env(&mut stack) {
|
||||
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);
|
||||
|
||||
start_time = std::time::Instant::now();
|
||||
@ -306,20 +322,26 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
perf!("reset signals", start_time, use_color);
|
||||
|
||||
start_time = std::time::Instant::now();
|
||||
// Right before we start our prompt and take input from the user,
|
||||
// fire the "pre_prompt" hook
|
||||
if let Some(hook) = engine_state.get_config().hooks.pre_prompt.clone() {
|
||||
if let Err(err) = eval_hook(engine_state, &mut stack, None, vec![], &hook, "pre_prompt") {
|
||||
report_shell_error(engine_state, &err);
|
||||
}
|
||||
// Right before we start our prompt and take input from the user, fire the "pre_prompt" hook
|
||||
if let Err(err) = hook::eval_hooks(
|
||||
engine_state,
|
||||
&mut stack,
|
||||
vec![],
|
||||
&engine_state.get_config().hooks.pre_prompt.clone(),
|
||||
"pre_prompt",
|
||||
) {
|
||||
report_shell_error(engine_state, &err);
|
||||
}
|
||||
perf!("pre-prompt hook", start_time, use_color);
|
||||
|
||||
start_time = std::time::Instant::now();
|
||||
// Next, check all the environment variables they ask for
|
||||
// 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(env_change, engine_state, &mut stack) {
|
||||
if let Err(error) = hook::eval_env_change_hook(
|
||||
&engine_state.get_config().hooks.env_change.clone(),
|
||||
engine_state,
|
||||
&mut stack,
|
||||
) {
|
||||
report_shell_error(engine_state, &error)
|
||||
}
|
||||
perf!("env-change hook", start_time, use_color);
|
||||
@ -362,7 +384,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
)))
|
||||
.with_quick_completions(config.completions.quick)
|
||||
.with_partial_completions(config.completions.partial)
|
||||
.with_ansi_colors(config.use_ansi_coloring)
|
||||
.with_ansi_colors(config.use_ansi_coloring.get(engine_state))
|
||||
.with_cwd(Some(
|
||||
engine_state
|
||||
.cwd(None)
|
||||
@ -382,7 +404,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
|
||||
|
||||
start_time = std::time::Instant::now();
|
||||
line_editor = if config.use_ansi_coloring {
|
||||
line_editor = if config.use_ansi_coloring.get(engine_state) {
|
||||
line_editor.with_hinter(Box::new({
|
||||
// As of Nov 2022, "hints" color_config closures only get `null` passed in.
|
||||
let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
|
||||
@ -504,18 +526,17 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
|
||||
// Right before we start running the code the user gave us, fire the `pre_execution`
|
||||
// hook
|
||||
if let Some(hook) = config.hooks.pre_execution.clone() {
|
||||
{
|
||||
// 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");
|
||||
repl.buffer = repl_cmd_line_text.to_string();
|
||||
drop(repl);
|
||||
|
||||
if let Err(err) = eval_hook(
|
||||
if let Err(err) = hook::eval_hooks(
|
||||
engine_state,
|
||||
&mut stack,
|
||||
None,
|
||||
vec![],
|
||||
&hook,
|
||||
&engine_state.get_config().hooks.pre_execution.clone(),
|
||||
"pre_execution",
|
||||
) {
|
||||
report_shell_error(engine_state, &err);
|
||||
@ -750,7 +771,7 @@ fn fill_in_result_related_history_metadata(
|
||||
c.duration = Some(cmd_duration);
|
||||
c.exit_status = stack
|
||||
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
||||
.and_then(|e| e.as_i64().ok());
|
||||
.and_then(|e| e.as_int().ok());
|
||||
c
|
||||
})
|
||||
.into_diagnostic()?; // todo: don't stop repl if error here?
|
||||
@ -789,8 +810,10 @@ fn parse_operation(
|
||||
) -> Result<ReplOperation, ErrReport> {
|
||||
let tokens = lex(s.as_bytes(), 0, &[], &[], false);
|
||||
// Check if this is a single call to a directory, if so auto-cd
|
||||
#[allow(deprecated)]
|
||||
let cwd = nu_engine::env::current_dir_str(engine_state, stack).unwrap_or_default();
|
||||
let cwd = engine_state
|
||||
.cwd(Some(stack))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
let mut orig = s.clone();
|
||||
if orig.starts_with('`') {
|
||||
orig = trim_quotes_str(&orig).to_string()
|
||||
@ -824,21 +847,26 @@ fn do_auto_cd(
|
||||
if !path.exists() {
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::DirectoryNotFound {
|
||||
dir: path.to_string_lossy().to_string(),
|
||||
&ShellError::Io(IoError::new_with_additional_context(
|
||||
std::io::ErrorKind::NotFound,
|
||||
span,
|
||||
},
|
||||
PathBuf::from(&path),
|
||||
"Cannot change directory",
|
||||
)),
|
||||
);
|
||||
}
|
||||
path.to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
if let PermissionResult::PermissionDenied(reason) = have_permission(path.clone()) {
|
||||
if let PermissionResult::PermissionDenied(_) = have_permission(path.clone()) {
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::IOError {
|
||||
msg: format!("Cannot change directory to {path}: {reason}"),
|
||||
},
|
||||
&ShellError::Io(IoError::new_with_additional_context(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
span,
|
||||
PathBuf::from(path),
|
||||
"Cannot change directory",
|
||||
)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -949,8 +977,7 @@ fn run_shell_integration_osc2(
|
||||
stack: &mut Stack,
|
||||
use_color: bool,
|
||||
) {
|
||||
#[allow(deprecated)]
|
||||
if let Ok(path) = current_dir_str(engine_state, stack) {
|
||||
if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Try to abbreviate string for windows title
|
||||
@ -994,8 +1021,7 @@ fn run_shell_integration_osc7(
|
||||
stack: &mut Stack,
|
||||
use_color: bool,
|
||||
) {
|
||||
#[allow(deprecated)]
|
||||
if let Ok(path) = current_dir_str(engine_state, stack) {
|
||||
if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir)
|
||||
@ -1018,8 +1044,7 @@ fn run_shell_integration_osc7(
|
||||
}
|
||||
|
||||
fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
|
||||
#[allow(deprecated)]
|
||||
if let Ok(path) = current_dir_str(engine_state, stack) {
|
||||
if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Otherwise, communicate the path as OSC 9;9 from ConEmu (often used for spawning new tabs in the same dir)
|
||||
@ -1043,8 +1068,7 @@ fn run_shell_integration_osc633(
|
||||
use_color: bool,
|
||||
repl_cmd_line_text: String,
|
||||
) {
|
||||
#[allow(deprecated)]
|
||||
if let Ok(path) = current_dir_str(engine_state, stack) {
|
||||
if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
|
||||
// Supported escape sequences of Microsoft's Visual Studio Code (vscode)
|
||||
// https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
|
||||
if stack
|
||||
@ -1069,16 +1093,8 @@ fn run_shell_integration_osc633(
|
||||
|
||||
// escape a few things because this says so
|
||||
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
||||
|
||||
let replaced_cmd_text: String = repl_cmd_line_text
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'\n' => '\x0a',
|
||||
'\r' => '\x0d',
|
||||
'\x1b' => '\x1b',
|
||||
_ => c,
|
||||
})
|
||||
.collect();
|
||||
let replaced_cmd_text =
|
||||
escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
|
||||
|
||||
//OSC 633 ; E ; <commandline> [; <nonce] ST - Explicitly set the command line with an optional nonce.
|
||||
run_ansi_sequence(&format!(
|
||||
@ -1143,7 +1159,7 @@ fn setup_history(
|
||||
/// Setup Reedline keybindingds based on the provided config
|
||||
///
|
||||
fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
|
||||
return match create_keybindings(engine_state.get_config()) {
|
||||
match create_keybindings(engine_state.get_config()) {
|
||||
Ok(keybindings) => match keybindings {
|
||||
KeybindingsMode::Emacs(keybindings) => {
|
||||
let edit_mode = Box::new(Emacs::new(keybindings));
|
||||
@ -1161,7 +1177,7 @@ fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedl
|
||||
report_shell_error(engine_state, &e);
|
||||
line_editor
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
@ -1245,7 +1261,7 @@ fn get_command_finished_marker(
|
||||
) -> String {
|
||||
let exit_code = stack
|
||||
.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 stack
|
||||
@ -1356,10 +1372,9 @@ fn run_finaliziation_ansi_sequence(
|
||||
|
||||
// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
|
||||
#[cfg(windows)]
|
||||
static DRIVE_PATH_REGEX: once_cell::sync::Lazy<fancy_regex::Regex> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
|
||||
});
|
||||
static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
|
||||
fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
|
||||
});
|
||||
|
||||
// A best-effort "does this string look kinda like a path?" function to determine whether to auto-cd
|
||||
fn looks_like_path(orig: &str) -> bool {
|
||||
@ -1421,7 +1436,7 @@ fn are_session_ids_in_sync() {
|
||||
|
||||
#[cfg(test)]
|
||||
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_protocol::engine::{EngineState, Stack};
|
||||
use tempfile::tempdir;
|
||||
@ -1559,6 +1574,13 @@ mod test_auto_cd {
|
||||
symlink(&dir, &link).unwrap();
|
||||
let input = if cfg!(windows) { r".\link" } else { "./link" };
|
||||
check(tempdir, input, link);
|
||||
|
||||
let dir = tempdir.join("foo").join("bar");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let link = tempdir.join("link2");
|
||||
symlink(&dir, &link).unwrap();
|
||||
let input = "..";
|
||||
check(link, input, tempdir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1571,4 +1593,43 @@ mod test_auto_cd {
|
||||
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
|
||||
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::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::Custom(..) => 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_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::{
|
||||
cli_error::report_compile_error,
|
||||
debugger::WithoutDebug,
|
||||
@ -10,7 +12,7 @@ use nu_protocol::{
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use nu_utils::enable_vt_processing;
|
||||
use nu_utils::perf;
|
||||
use nu_utils::{escape_quote_string, perf};
|
||||
use std::path::Path;
|
||||
|
||||
// This will collect environment variables from std::env and adds them to a stack.
|
||||
@ -130,7 +132,7 @@ fn gather_env_vars(
|
||||
working_set.error(err);
|
||||
}
|
||||
|
||||
if working_set.parse_errors.first().is_some() {
|
||||
if !working_set.parse_errors.is_empty() {
|
||||
report_capture_error(
|
||||
engine_state,
|
||||
&String::from_utf8_lossy(contents),
|
||||
@ -174,7 +176,7 @@ fn gather_env_vars(
|
||||
working_set.error(err);
|
||||
}
|
||||
|
||||
if working_set.parse_errors.first().is_some() {
|
||||
if !working_set.parse_errors.is_empty() {
|
||||
report_capture_error(
|
||||
engine_state,
|
||||
&String::from_utf8_lossy(contents),
|
||||
@ -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(
|
||||
engine_state: &mut EngineState,
|
||||
stack: &mut Stack,
|
||||
@ -234,7 +265,10 @@ pub fn eval_source(
|
||||
perf!(
|
||||
&format!("eval_source {}", &fname),
|
||||
start_time,
|
||||
engine_state.get_config().use_ansi_coloring
|
||||
engine_state
|
||||
.get_config()
|
||||
.use_ansi_coloring
|
||||
.get(engine_state)
|
||||
);
|
||||
|
||||
exit_code
|
||||
@ -267,7 +301,7 @@ fn evaluate_source(
|
||||
|
||||
if let Some(err) = working_set.compile_errors.first() {
|
||||
report_compile_error(&working_set, err);
|
||||
// Not a fatal error, for now
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
(output, working_set.render())
|
||||
@ -281,21 +315,8 @@ fn evaluate_source(
|
||||
eval_block::<WithoutDebug>(engine_state, stack, &block, input)
|
||||
}?;
|
||||
|
||||
if let PipelineData::ByteStream(..) = pipeline {
|
||||
pipeline.print(engine_state, stack, false, false)
|
||||
} 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)
|
||||
}?;
|
||||
let no_newline = matches!(&pipeline, &PipelineData::ByteStream(..));
|
||||
print_pipeline(engine_state, stack, pipeline, no_newline)?;
|
||||
|
||||
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 nu_highlight;
|
||||
|
@ -62,29 +62,29 @@ fn extern_completer() -> NuCompleter {
|
||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn completer_strings_with_options() -> NuCompleter {
|
||||
// Create a new engine
|
||||
fn custom_completer_with_options(
|
||||
global_opts: &str,
|
||||
completer_opts: &str,
|
||||
completions: &[&str],
|
||||
) -> NuCompleter {
|
||||
let (_, _, mut engine, mut stack) = new_engine();
|
||||
// Add record value as example
|
||||
let record = r#"
|
||||
# To test that the config setting has no effect on the custom completions
|
||||
$env.config.completions.algorithm = "fuzzy"
|
||||
def animals [] {
|
||||
{
|
||||
# Very rare and totally real animals
|
||||
completions: ["Abcdef", "Foo Abcdef", "Acd Bar" ],
|
||||
options: {
|
||||
completion_algorithm: "prefix",
|
||||
positional: false,
|
||||
case_sensitive: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
def my-command [animal: string@animals] { print $animal }"#;
|
||||
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||
let command = format!(
|
||||
r#"
|
||||
{}
|
||||
def comp [] {{
|
||||
{{ completions: [{}], options: {{ {} }} }}
|
||||
}}
|
||||
def my-command [arg: string@comp] {{}}"#,
|
||||
global_opts,
|
||||
completions
|
||||
.iter()
|
||||
.map(|comp| format!("'{}'", comp))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
completer_opts,
|
||||
);
|
||||
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))
|
||||
}
|
||||
|
||||
@ -111,25 +111,6 @@ fn custom_completer() -> NuCompleter {
|
||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn subcommand_completer() -> NuCompleter {
|
||||
// Create a new engine
|
||||
let (_, _, mut engine, mut stack) = new_engine();
|
||||
|
||||
let commands = r#"
|
||||
$env.config.completions.algorithm = "fuzzy"
|
||||
def foo [] {}
|
||||
def "foo bar" [] {}
|
||||
def "foo abaz" [] {}
|
||||
def "foo aabcrr" [] {}
|
||||
def food [] {}
|
||||
"#;
|
||||
assert!(support::merge_input(commands.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||
|
||||
// Instantiate a new completer
|
||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||
}
|
||||
|
||||
/// Use fuzzy completions but sort in alphabetical order
|
||||
#[fixture]
|
||||
fn fuzzy_alpha_sort_completer() -> NuCompleter {
|
||||
@ -155,7 +136,7 @@ fn variables_dollar_sign_with_variablecompletion() {
|
||||
let target_dir = "$ ";
|
||||
let suggestions = completer.complete(target_dir, target_dir.len());
|
||||
|
||||
assert_eq!(8, suggestions.len());
|
||||
assert_eq!(9, suggestions.len());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@ -196,20 +177,94 @@ fn variables_customcompletion_subcommands_with_customcompletion_2(
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn customcompletions_substring_matching(mut completer_strings_with_options: NuCompleter) {
|
||||
let suggestions = completer_strings_with_options.complete("my-command Abcd", 15);
|
||||
/// $env.config should be overridden by the custom completer's options
|
||||
#[test]
|
||||
fn customcompletions_override_options() {
|
||||
let mut completer = custom_completer_with_options(
|
||||
r#"$env.config.completions.algorithm = "fuzzy"
|
||||
$env.config.completions.case_sensitive = false"#,
|
||||
r#"completion_algorithm: "prefix",
|
||||
positional: false,
|
||||
case_sensitive: true,
|
||||
sort: true"#,
|
||||
&["Foo Abcdef", "Abcdef", "Acd Bar"],
|
||||
);
|
||||
|
||||
// positional: false should make it do substring matching
|
||||
// sort: true should force sorting
|
||||
let expected: Vec<String> = vec!["Abcdef".into(), "Foo Abcdef".into()];
|
||||
let suggestions = completer.complete("my-command Abcd", 15);
|
||||
match_suggestions(&expected, &suggestions);
|
||||
|
||||
// Custom options should make case-sensitive
|
||||
let suggestions = completer.complete("my-command aBcD", 15);
|
||||
assert!(suggestions.is_empty());
|
||||
}
|
||||
|
||||
/// $env.config should be inherited by the custom completer's options
|
||||
#[test]
|
||||
fn customcompletions_inherit_options() {
|
||||
let mut completer = custom_completer_with_options(
|
||||
r#"$env.config.completions.algorithm = "fuzzy"
|
||||
$env.config.completions.case_sensitive = false"#,
|
||||
"",
|
||||
&["Foo Abcdef", "Abcdef", "Acd Bar"],
|
||||
);
|
||||
|
||||
// Make sure matching is fuzzy
|
||||
let suggestions = completer.complete("my-command Acd", 14);
|
||||
let expected: Vec<String> = vec!["Acd Bar".into(), "Abcdef".into(), "Foo Abcdef".into()];
|
||||
match_suggestions(&expected, &suggestions);
|
||||
|
||||
// Custom options should make matching case insensitive
|
||||
let suggestions = completer.complete("my-command acd", 14);
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn customcompletions_case_insensitive(mut completer_strings_with_options: NuCompleter) {
|
||||
let suggestions = completer_strings_with_options.complete("my-command foo", 14);
|
||||
let expected: Vec<String> = vec!["Foo Abcdef".into()];
|
||||
#[test]
|
||||
fn customcompletions_no_sort() {
|
||||
let mut completer = custom_completer_with_options(
|
||||
"",
|
||||
r#"completion_algorithm: "fuzzy",
|
||||
sort: false"#,
|
||||
&["zzzfoo", "foo", "not matched", "abcfoo"],
|
||||
);
|
||||
let suggestions = completer.complete("my-command foo", 14);
|
||||
let expected: Vec<String> = vec!["zzzfoo".into(), "foo".into(), "abcfoo".into()];
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
/// Fallback to file completions if custom completer returns null
|
||||
#[test]
|
||||
fn customcompletions_fallback() {
|
||||
let (_, _, mut engine, mut stack) = new_engine();
|
||||
let command = r#"
|
||||
def comp [] { null }
|
||||
def my-command [arg: string@comp] {}"#;
|
||||
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
let completion_str = "my-command test";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
let expected: Vec<String> = vec![folder("test_a"), file("test_a_symlink"), folder("test_b")];
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
/// Suppress completions for invalid values
|
||||
#[test]
|
||||
fn customcompletions_invalid() {
|
||||
let (_, _, mut engine, mut stack) = new_engine();
|
||||
let command = r#"
|
||||
def comp [] { 123 }
|
||||
def my-command [arg: string@comp] {}"#;
|
||||
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
|
||||
let completion_str = "my-command foo";
|
||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||
assert!(suggestions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dotnu_completions() {
|
||||
// Create a new engine
|
||||
@ -218,6 +273,33 @@ fn dotnu_completions() {
|
||||
// Instantiate a new completer
|
||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
|
||||
// Test nested nu script
|
||||
#[cfg(windows)]
|
||||
let completion_str = "use `.\\dir_module\\".to_string();
|
||||
#[cfg(not(windows))]
|
||||
let completion_str = "use `./dir_module/".to_string();
|
||||
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||
|
||||
match_suggestions(
|
||||
&vec![
|
||||
"mod.nu".into(),
|
||||
#[cfg(windows)]
|
||||
"sub module\\`".into(),
|
||||
#[cfg(not(windows))]
|
||||
"sub module/`".into(),
|
||||
],
|
||||
&suggestions,
|
||||
);
|
||||
|
||||
// Test nested nu script, with ending '`'
|
||||
#[cfg(windows)]
|
||||
let completion_str = "use `.\\dir_module\\sub module\\`".to_string();
|
||||
#[cfg(not(windows))]
|
||||
let completion_str = "use `./dir_module/sub module/`".to_string();
|
||||
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||
|
||||
match_suggestions(&vec!["sub.nu`".into()], &suggestions);
|
||||
|
||||
let expected = vec![
|
||||
"asdf.nu".into(),
|
||||
"bar.nu".into(),
|
||||
@ -228,6 +310,18 @@ fn dotnu_completions() {
|
||||
#[cfg(not(windows))]
|
||||
"dir_module/".into(),
|
||||
"foo.nu".into(),
|
||||
#[cfg(windows)]
|
||||
"lib-dir1\\".into(),
|
||||
#[cfg(not(windows))]
|
||||
"lib-dir1/".into(),
|
||||
#[cfg(windows)]
|
||||
"lib-dir2\\".into(),
|
||||
#[cfg(not(windows))]
|
||||
"lib-dir2/".into(),
|
||||
#[cfg(windows)]
|
||||
"lib-dir3\\".into(),
|
||||
#[cfg(not(windows))]
|
||||
"lib-dir3/".into(),
|
||||
"spam.nu".into(),
|
||||
"xyzzy.nu".into(),
|
||||
];
|
||||
@ -288,6 +382,27 @@ fn external_completer_pass_flags() {
|
||||
assert_eq!("--", suggestions.get(2).unwrap().value);
|
||||
}
|
||||
|
||||
/// Fallback to file completions when external completer returns null
|
||||
#[test]
|
||||
fn external_completer_fallback() {
|
||||
let block = "{|spans| null}";
|
||||
let input = "foo test".to_string();
|
||||
|
||||
let expected = vec![folder("test_a"), file("test_a_symlink"), folder("test_b")];
|
||||
let suggestions = run_external_completion(block, &input);
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
/// Suppress completions when external completer returns invalid value
|
||||
#[test]
|
||||
fn external_completer_invalid() {
|
||||
let block = "{|spans| 123}";
|
||||
let input = "foo ".to_string();
|
||||
|
||||
let suggestions = run_external_completion(block, &input);
|
||||
assert!(suggestions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_completions() {
|
||||
// Create a new engine
|
||||
@ -307,6 +422,7 @@ fn file_completions() {
|
||||
folder(dir.join("directory_completion")),
|
||||
file(dir.join("nushell")),
|
||||
folder(dir.join("test_a")),
|
||||
file(dir.join("test_a_symlink")),
|
||||
folder(dir.join("test_b")),
|
||||
file(dir.join(".hidden_file")),
|
||||
folder(dir.join(".hidden_folder")),
|
||||
@ -329,6 +445,40 @@ fn file_completions() {
|
||||
// Match the results
|
||||
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")),
|
||||
file(dir.join("test_a_symlink")),
|
||||
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
|
||||
let target_dir = format!("cp {}", folder(dir.join("another")));
|
||||
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||
@ -363,6 +513,77 @@ fn file_completions() {
|
||||
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")),
|
||||
file(dir.join("test_a_symlink")),
|
||||
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")),
|
||||
file(dir.join("test_a_symlink")),
|
||||
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)]
|
||||
#[test]
|
||||
fn file_completions_with_mixed_separators() {
|
||||
@ -633,6 +854,7 @@ fn command_ls_with_filecompletion() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -644,6 +866,7 @@ fn command_ls_with_filecompletion() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -675,6 +898,7 @@ fn command_open_with_filecompletion() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -686,6 +910,7 @@ fn command_open_with_filecompletion() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -717,6 +942,7 @@ fn command_rm_with_globcompletion() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -728,6 +954,7 @@ fn command_rm_with_globcompletion() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -752,6 +979,7 @@ fn command_cp_with_globcompletion() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -763,6 +991,7 @@ fn command_cp_with_globcompletion() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -787,6 +1016,7 @@ fn command_save_with_filecompletion() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -798,6 +1028,7 @@ fn command_save_with_filecompletion() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -822,6 +1053,7 @@ fn command_touch_with_filecompletion() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -833,6 +1065,7 @@ fn command_touch_with_filecompletion() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -857,6 +1090,7 @@ fn command_watch_with_filecompletion() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -868,6 +1102,7 @@ fn command_watch_with_filecompletion() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -877,24 +1112,32 @@ fn command_watch_with_filecompletion() {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn subcommand_completions(mut subcommand_completer: NuCompleter) {
|
||||
let prefix = "foo br";
|
||||
let suggestions = subcommand_completer.complete(prefix, prefix.len());
|
||||
match_suggestions(
|
||||
&vec!["foo bar".to_string(), "foo aabcrr".to_string()],
|
||||
&suggestions,
|
||||
);
|
||||
fn subcommand_completions() {
|
||||
let (_, _, mut engine, mut stack) = new_engine();
|
||||
let commands = r#"
|
||||
$env.config.completions.algorithm = "fuzzy"
|
||||
def foo-test-command [] {}
|
||||
def "foo-test-command bar" [] {}
|
||||
def "foo-test-command aagap bcr" [] {}
|
||||
def "food bar" [] {}
|
||||
"#;
|
||||
assert!(support::merge_input(commands.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||
let mut subcommand_completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||
|
||||
let prefix = "foo b";
|
||||
let prefix = "fod br";
|
||||
let suggestions = subcommand_completer.complete(prefix, prefix.len());
|
||||
match_suggestions(
|
||||
&vec![
|
||||
"foo bar".to_string(),
|
||||
"foo aabcrr".to_string(),
|
||||
"foo abaz".to_string(),
|
||||
"food bar".to_string(),
|
||||
"foo-test-command bar".to_string(),
|
||||
"foo-test-command aagap bcr".to_string(),
|
||||
],
|
||||
&suggestions,
|
||||
);
|
||||
|
||||
let prefix = "foot bar";
|
||||
let suggestions = subcommand_completer.complete(prefix, prefix.len());
|
||||
match_suggestions(&vec!["foo-test-command bar".to_string()], &suggestions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -955,8 +1198,8 @@ fn flag_completions() {
|
||||
"--mime-type".into(),
|
||||
"--short-names".into(),
|
||||
"--threads".into(),
|
||||
"-D".into(),
|
||||
"-a".into(),
|
||||
"-D".into(),
|
||||
"-d".into(),
|
||||
"-f".into(),
|
||||
"-h".into(),
|
||||
@ -1209,7 +1452,7 @@ fn variables_completions() {
|
||||
// Test completions for $nu
|
||||
let suggestions = completer.complete("$nu.", 4);
|
||||
|
||||
assert_eq!(18, suggestions.len());
|
||||
assert_eq!(19, suggestions.len());
|
||||
|
||||
let expected: Vec<String> = vec![
|
||||
"cache-dir".into(),
|
||||
@ -1229,6 +1472,7 @@ fn variables_completions() {
|
||||
"plugin-path".into(),
|
||||
"startup-time".into(),
|
||||
"temp-path".into(),
|
||||
"user-autoload-dirs".into(),
|
||||
"vendor-autoload-dirs".into(),
|
||||
];
|
||||
|
||||
@ -1287,7 +1531,7 @@ fn variables_completions() {
|
||||
assert_eq!(3, suggestions.len());
|
||||
|
||||
#[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))]
|
||||
let expected: Vec<String> = vec!["PATH".into(), "PWD".into(), "TEST".into()];
|
||||
|
||||
@ -1322,9 +1566,17 @@ fn alias_of_command_and_flags() {
|
||||
|
||||
let suggestions = completer.complete("ll t", 4);
|
||||
#[cfg(windows)]
|
||||
let expected_paths: Vec<String> = vec!["test_a\\".to_string(), "test_b\\".to_string()];
|
||||
let expected_paths: Vec<String> = vec![
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
];
|
||||
#[cfg(not(windows))]
|
||||
let expected_paths: Vec<String> = vec!["test_a/".to_string(), "test_b/".to_string()];
|
||||
let expected_paths: Vec<String> = vec![
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
];
|
||||
|
||||
match_suggestions(&expected_paths, &suggestions)
|
||||
}
|
||||
@ -1341,9 +1593,17 @@ fn alias_of_basic_command() {
|
||||
|
||||
let suggestions = completer.complete("ll t", 4);
|
||||
#[cfg(windows)]
|
||||
let expected_paths: Vec<String> = vec!["test_a\\".to_string(), "test_b\\".to_string()];
|
||||
let expected_paths: Vec<String> = vec![
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
];
|
||||
#[cfg(not(windows))]
|
||||
let expected_paths: Vec<String> = vec!["test_a/".to_string(), "test_b/".to_string()];
|
||||
let expected_paths: Vec<String> = vec![
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
];
|
||||
|
||||
match_suggestions(&expected_paths, &suggestions)
|
||||
}
|
||||
@ -1363,9 +1623,17 @@ fn alias_of_another_alias() {
|
||||
|
||||
let suggestions = completer.complete("lf t", 4);
|
||||
#[cfg(windows)]
|
||||
let expected_paths: Vec<String> = vec!["test_a\\".to_string(), "test_b\\".to_string()];
|
||||
let expected_paths: Vec<String> = vec![
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
];
|
||||
#[cfg(not(windows))]
|
||||
let expected_paths: Vec<String> = vec!["test_a/".to_string(), "test_b/".to_string()];
|
||||
let expected_paths: Vec<String> = vec![
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
];
|
||||
|
||||
match_suggestions(&expected_paths, &suggestions)
|
||||
}
|
||||
@ -1414,6 +1682,7 @@ fn unknown_command_completion() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -1425,6 +1694,7 @@ fn unknown_command_completion() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -1476,6 +1746,7 @@ fn filecompletions_triggers_after_cursor() {
|
||||
"directory_completion\\".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a\\".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b\\".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder\\".to_string(),
|
||||
@ -1487,6 +1758,7 @@ fn filecompletions_triggers_after_cursor() {
|
||||
"directory_completion/".to_string(),
|
||||
"nushell".to_string(),
|
||||
"test_a/".to_string(),
|
||||
"test_a_symlink".to_string(),
|
||||
"test_b/".to_string(),
|
||||
".hidden_file".to_string(),
|
||||
".hidden_folder/".to_string(),
|
||||
@ -1576,6 +1848,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"]
|
||||
#[rstest]
|
||||
fn alias_offset_bug_7648() {
|
||||
@ -1613,12 +1902,30 @@ fn alias_offset_bug_7754() {
|
||||
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());
|
||||
#[rstest]
|
||||
fn nested_block(mut completer: NuCompleter) {
|
||||
let expected: Vec<String> = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()];
|
||||
|
||||
let suggestions = completer.complete("somecmd | lines | each { tst - }", 30);
|
||||
match_suggestions(&expected, &suggestions);
|
||||
|
||||
let suggestions = completer.complete("somecmd | lines | each { tst -}", 30);
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn incomplete_nested_block(mut completer: NuCompleter) {
|
||||
let suggestions = completer.complete("somecmd | lines | each { tst -", 30);
|
||||
let expected: Vec<String> = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()];
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn deeply_nested_block(mut completer: NuCompleter) {
|
||||
let suggestions = completer.complete(
|
||||
"somecmd | lines | each { print ([each (print) (tst -)]) }",
|
||||
52,
|
||||
);
|
||||
let expected: Vec<String> = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()];
|
||||
match_suggestions(&expected, &suggestions);
|
||||
}
|
||||
|
@ -98,14 +98,14 @@ pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
||||
|
||||
stack.add_env_var(
|
||||
"NU_LIB_DIRS".to_string(),
|
||||
Value::List {
|
||||
vals: vec![
|
||||
Value::list(
|
||||
vec![
|
||||
Value::string(file(dir.join("lib-dir1")), dir_span),
|
||||
Value::string(file(dir.join("lib-dir2")), dir_span),
|
||||
Value::string(file(dir.join("lib-dir3")), dir_span),
|
||||
],
|
||||
internal_span: dir_span,
|
||||
},
|
||||
dir_span,
|
||||
),
|
||||
);
|
||||
|
||||
// Merge environment into the permanent state
|
||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-base"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
|
||||
version = "0.99.1"
|
||||
version = "0.102.0"
|
||||
|
||||
# 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
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
||||
nu-path = { path = "../nu-path", version = "0.99.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.102.0", default-features = false }
|
||||
nu-parser = { path = "../nu-parser", version = "0.102.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.102.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.102.0", default-features = false }
|
||||
|
||||
indexmap = { workspace = true }
|
||||
miette = { workspace = true }
|
||||
|
@ -7,49 +7,55 @@ use nu_protocol::{
|
||||
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
||||
PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
pub fn eval_env_change_hook(
|
||||
env_change_hook: Option<Value>,
|
||||
env_change_hook: &HashMap<String, Vec<Value>>,
|
||||
engine_state: &mut EngineState,
|
||||
stack: &mut Stack,
|
||||
) -> Result<(), ShellError> {
|
||||
if let Some(hook) = env_change_hook {
|
||||
match hook {
|
||||
Value::Record { val, .. } => {
|
||||
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 {
|
||||
let before = before.cloned().unwrap_or_default();
|
||||
let after = after.cloned().unwrap_or_default();
|
||||
for (env, hooks) in env_change_hook {
|
||||
let before = engine_state.previous_env_vars.get(env);
|
||||
let after = stack.get_env_var(engine_state, env);
|
||||
if before != after {
|
||||
let before = before.cloned().unwrap_or_default();
|
||||
let after = after.cloned().unwrap_or_default();
|
||||
|
||||
eval_hook(
|
||||
engine_state,
|
||||
stack,
|
||||
None,
|
||||
vec![("$before".into(), before), ("$after".into(), after.clone())],
|
||||
hook_value,
|
||||
"env_change",
|
||||
)?;
|
||||
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_name.clone(), after);
|
||||
}
|
||||
}
|
||||
}
|
||||
x => {
|
||||
return Err(ShellError::TypeMismatch {
|
||||
err_message: "record for the 'env_change' hook".to_string(),
|
||||
span: x.span(),
|
||||
});
|
||||
}
|
||||
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(
|
||||
engine_state,
|
||||
stack,
|
||||
None,
|
||||
arguments.clone(),
|
||||
hook,
|
||||
&format!("{hook_name} list, recursive"),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn eval_hook(
|
||||
engine_state: &mut EngineState,
|
||||
stack: &mut Stack,
|
||||
@ -127,16 +133,7 @@ pub fn eval_hook(
|
||||
}
|
||||
}
|
||||
Value::List { vals, .. } => {
|
||||
for val in vals {
|
||||
eval_hook(
|
||||
engine_state,
|
||||
stack,
|
||||
None,
|
||||
arguments.clone(),
|
||||
val,
|
||||
&format!("{hook_name} list, recursive"),
|
||||
)?;
|
||||
}
|
||||
eval_hooks(engine_state, stack, arguments, vals, hook_name)?;
|
||||
}
|
||||
Value::Record { val, .. } => {
|
||||
// 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")
|
||||
{
|
||||
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") {
|
||||
get_editor_commandline(value, "$env.VISUAL")
|
||||
} else if let Some(value) = env_vars.get("EDITOR") {
|
||||
get_editor_commandline(value, "$env.EDITOR")
|
||||
} else {
|
||||
Err(ShellError::GenericError {
|
||||
error: "No editor configured".into(),
|
||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-extra"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
|
||||
version = "0.99.1"
|
||||
version = "0.102.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -16,13 +16,13 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.99.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
||||
nu-json = { version = "0.99.1", path = "../nu-json" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
||||
nu-pretty-hex = { version = "0.99.1", path = "../nu-pretty-hex" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.99.1" }
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.102.0" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.102.0", default-features = false }
|
||||
nu-json = { version = "0.102.0", path = "../nu-json" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.102.0" }
|
||||
nu-pretty-hex = { version = "0.102.0", path = "../nu-pretty-hex" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.102.0", default-features = false }
|
||||
nu-utils = { path = "../nu-utils", version = "0.102.0", default-features = false }
|
||||
|
||||
# Potential dependencies for extras
|
||||
heck = { workspace = true }
|
||||
@ -34,8 +34,9 @@ serde = { workspace = true }
|
||||
serde_urlencoded = { workspace = true }
|
||||
v_htmlescape = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
mime = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.99.1" }
|
||||
nu-command = { path = "../nu-command", version = "0.99.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.99.1" }
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.102.0" }
|
||||
nu-command = { path = "../nu-command", version = "0.102.0" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.102.0" }
|
||||
|
@ -43,7 +43,12 @@ mod test_examples {
|
||||
signature.operates_on_cell_paths(),
|
||||
),
|
||||
);
|
||||
check_example_evaluates_to_expected_output(&example, cwd.as_path(), &mut engine_state);
|
||||
check_example_evaluates_to_expected_output(
|
||||
cmd.name(),
|
||||
&example,
|
||||
cwd.as_path(),
|
||||
&mut engine_state,
|
||||
);
|
||||
}
|
||||
|
||||
check_all_signature_input_output_types_entries_have_examples(
|
||||
|
@ -1,20 +1,6 @@
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
use nu_protocol::Signals;
|
||||
use num_traits::ToPrimitive;
|
||||
|
||||
pub struct Arguments {
|
||||
cell_paths: Option<Vec<CellPath>>,
|
||||
}
|
||||
|
||||
impl CmdArgument for Arguments {
|
||||
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
|
||||
self.cell_paths.take()
|
||||
}
|
||||
}
|
||||
use nu_protocol::{report_parse_warning, ParseWarning};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BitsInto;
|
||||
@ -42,15 +28,15 @@ impl Command for BitsInto {
|
||||
SyntaxShape::CellPath,
|
||||
"for a data structure input, convert data at the given cell paths",
|
||||
)
|
||||
.category(Category::Conversions)
|
||||
.category(Category::Deprecated)
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Convert value to a binary primitive."
|
||||
"Convert value to a binary string."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["convert", "cast"]
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn run(
|
||||
@ -60,7 +46,17 @@ impl Command for BitsInto {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
into_bits(engine_state, stack, call, input)
|
||||
let head = call.head;
|
||||
report_parse_warning(
|
||||
&StateWorkingSet::new(engine_state),
|
||||
&ParseWarning::DeprecatedWarning {
|
||||
old_command: "into bits".into(),
|
||||
new_suggestion: "use `format bits`".into(),
|
||||
span: head,
|
||||
url: "`help format bits`".into(),
|
||||
},
|
||||
);
|
||||
crate::extra::strings::format::format_bits(engine_state, stack, call, input)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -111,126 +107,6 @@ impl Command for BitsInto {
|
||||
}
|
||||
}
|
||||
|
||||
fn into_bits(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let cell_paths = call.rest(engine_state, stack, 0)?;
|
||||
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||
|
||||
if let PipelineData::ByteStream(stream, metadata) = input {
|
||||
Ok(PipelineData::ByteStream(
|
||||
byte_stream_to_bits(stream, head),
|
||||
metadata,
|
||||
))
|
||||
} else {
|
||||
let args = Arguments { cell_paths };
|
||||
operate(action, args, input, call.head, engine_state.signals())
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_stream_to_bits(stream: ByteStream, head: Span) -> ByteStream {
|
||||
if let Some(mut reader) = stream.reader() {
|
||||
let mut is_first = true;
|
||||
ByteStream::from_fn(
|
||||
head,
|
||||
Signals::empty(),
|
||||
ByteStreamType::String,
|
||||
move |buffer| {
|
||||
let mut byte = [0];
|
||||
if reader.read(&mut byte[..]).err_span(head)? > 0 {
|
||||
// Format the byte as bits
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else {
|
||||
buffer.push(b' ');
|
||||
}
|
||||
write!(buffer, "{:08b}", byte[0]).expect("format failed");
|
||||
Ok(true)
|
||||
} else {
|
||||
// EOF
|
||||
Ok(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
ByteStream::read(io::empty(), head, Signals::empty(), ByteStreamType::String)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_to_smallest_number_type(num: i64, span: Span) -> Value {
|
||||
if let Some(v) = num.to_i8() {
|
||||
let bytes = v.to_ne_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
} else if let Some(v) = num.to_i16() {
|
||||
let bytes = v.to_ne_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
} else if let Some(v) = num.to_i32() {
|
||||
let bytes = v.to_ne_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
} else {
|
||||
let bytes = num.to_ne_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action(input: &Value, _args: &Arguments, span: Span) -> Value {
|
||||
match input {
|
||||
Value::Binary { val, .. } => {
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in val {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
}
|
||||
Value::Int { val, .. } => convert_to_smallest_number_type(*val, span),
|
||||
Value::Filesize { val, .. } => convert_to_smallest_number_type(*val, span),
|
||||
Value::Duration { val, .. } => convert_to_smallest_number_type(*val, span),
|
||||
Value::String { val, .. } => {
|
||||
let raw_bytes = val.as_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in raw_bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
}
|
||||
Value::Bool { val, .. } => {
|
||||
let v = <i64 as From<bool>>::from(*val);
|
||||
convert_to_smallest_number_type(v, span)
|
||||
}
|
||||
// Propagate errors by explicitly matching them before the final case.
|
||||
Value::Error { .. } => input.clone(),
|
||||
other => Value::error(
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "int, filesize, string, duration, binary, or bool".into(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: span,
|
||||
src_span: other.span(),
|
||||
},
|
||||
span,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{report_parse_warning, ParseWarning};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Fmt;
|
||||
@ -16,11 +16,11 @@ impl Command for Fmt {
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("fmt")
|
||||
.input_output_types(vec![(Type::Number, Type::record())])
|
||||
.category(Category::Conversions)
|
||||
.category(Category::Deprecated)
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["display", "render", "format"]
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -47,72 +47,20 @@ impl Command for Fmt {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
fmt(engine_state, stack, call, input)
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
|
||||
let args = CellPathOnlyArgs::from(cell_paths);
|
||||
operate(action, args, input, call.head, engine_state.signals())
|
||||
}
|
||||
|
||||
fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
|
||||
match input {
|
||||
Value::Float { val, .. } => fmt_it_64(*val, span),
|
||||
Value::Int { val, .. } => fmt_it(*val, span),
|
||||
Value::Filesize { val, .. } => fmt_it(*val, span),
|
||||
// Propagate errors by explicitly matching them before the final case.
|
||||
Value::Error { .. } => input.clone(),
|
||||
other => Value::error(
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "float, int, or filesize".into(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: span,
|
||||
src_span: other.span(),
|
||||
let head = call.head;
|
||||
report_parse_warning(
|
||||
&StateWorkingSet::new(engine_state),
|
||||
&ParseWarning::DeprecatedWarning {
|
||||
old_command: "fmt".into(),
|
||||
new_suggestion: "use `format number`".into(),
|
||||
span: head,
|
||||
url: "`help format number`".into(),
|
||||
},
|
||||
span,
|
||||
),
|
||||
);
|
||||
crate::extra::strings::format::format_number(engine_state, stack, call, input)
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_it(num: i64, span: Span) -> Value {
|
||||
Value::record(
|
||||
record! {
|
||||
"binary" => Value::string(format!("{num:#b}"), span),
|
||||
"debug" => Value::string(format!("{num:#?}"), span),
|
||||
"display" => Value::string(format!("{num}"), span),
|
||||
"lowerexp" => Value::string(format!("{num:#e}"), span),
|
||||
"lowerhex" => Value::string(format!("{num:#x}"), span),
|
||||
"octal" => Value::string(format!("{num:#o}"), span),
|
||||
"upperexp" => Value::string(format!("{num:#E}"), span),
|
||||
"upperhex" => Value::string(format!("{num:#X}"), span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
fn fmt_it_64(num: f64, span: Span) -> Value {
|
||||
Value::record(
|
||||
record! {
|
||||
"binary" => Value::string(format!("{:b}", num.to_bits()), span),
|
||||
"debug" => Value::string(format!("{num:#?}"), span),
|
||||
"display" => Value::string(format!("{num}"), span),
|
||||
"lowerexp" => Value::string(format!("{num:#e}"), span),
|
||||
"lowerhex" => Value::string(format!("{:0x}", num.to_bits()), span),
|
||||
"octal" => Value::string(format!("{:0o}", num.to_bits()), span),
|
||||
"upperexp" => Value::string(format!("{num:#E}"), span),
|
||||
"upperhex" => Value::string(format!("{:0X}", num.to_bits()), span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -25,7 +25,7 @@ impl Command for EachWhile {
|
||||
)])
|
||||
.required(
|
||||
"closure",
|
||||
SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])),
|
||||
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
|
||||
"the closure to run",
|
||||
)
|
||||
.category(Category::Filters)
|
||||
|
@ -13,6 +13,8 @@ impl Command for Rotate {
|
||||
.input_output_types(vec![
|
||||
(Type::record(), Type::table()),
|
||||
(Type::table(), Type::table()),
|
||||
(Type::list(Type::Any), Type::table()),
|
||||
(Type::String, Type::table()),
|
||||
])
|
||||
.switch("ccw", "rotate counter clockwise", None)
|
||||
.rest(
|
||||
@ -21,6 +23,7 @@ impl Command for Rotate {
|
||||
"the names to give columns once rotated",
|
||||
)
|
||||
.category(Category::Filters)
|
||||
.allow_variants_without_examples(true)
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
|
@ -2,4 +2,4 @@ mod from;
|
||||
mod to;
|
||||
|
||||
pub(crate) use from::url::FromUrl;
|
||||
pub(crate) use to::html::ToHtml;
|
||||
pub use to::html::ToHtml;
|
||||
|
@ -330,7 +330,12 @@ fn to_html(
|
||||
output_string = run_regexes(®ex_hm, &output_string);
|
||||
}
|
||||
|
||||
Ok(Value::string(output_string, head).into_pipeline_data())
|
||||
let metadata = PipelineMetadata {
|
||||
data_source: nu_protocol::DataSource::None,
|
||||
content_type: Some(mime::TEXT_HTML_UTF_8.to_string()),
|
||||
};
|
||||
|
||||
Ok(Value::string(output_string, head).into_pipeline_data_with_metadata(metadata))
|
||||
}
|
||||
|
||||
fn theme_demo(span: Span) -> PipelineData {
|
||||
|
@ -9,6 +9,7 @@ mod strings;
|
||||
pub use bits::{
|
||||
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::{MathCos, MathCosH, MathSin, MathSinH, MathTan, MathTanH};
|
||||
pub use math::{MathExp, MathLn};
|
||||
@ -45,6 +46,8 @@ pub fn add_extra_command_context(mut engine_state: EngineState) -> EngineState {
|
||||
|
||||
bind_command!(
|
||||
strings::format::FormatPattern,
|
||||
strings::format::FormatBits,
|
||||
strings::format::FormatNumber,
|
||||
strings::str_::case::Str,
|
||||
strings::str_::case::StrCamelCase,
|
||||
strings::str_::case::StrKebabCase,
|
||||
@ -54,7 +57,8 @@ pub fn add_extra_command_context(mut engine_state: EngineState) -> EngineState {
|
||||
strings::str_::case::StrTitleCase
|
||||
);
|
||||
|
||||
bind_command!(formats::ToHtml, formats::FromUrl);
|
||||
bind_command!(ToHtml, formats::FromUrl);
|
||||
|
||||
// Bits
|
||||
bind_command! {
|
||||
Bits,
|
||||
|
249
crates/nu-cmd-extra/src/extra/strings/format/bits.rs
Normal file
249
crates/nu-cmd-extra/src/extra/strings/format/bits.rs
Normal file
@ -0,0 +1,249 @@
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
use nu_protocol::{shell_error::io::IoError, Signals};
|
||||
use num_traits::ToPrimitive;
|
||||
|
||||
struct Arguments {
|
||||
cell_paths: Option<Vec<CellPath>>,
|
||||
}
|
||||
|
||||
impl CmdArgument for Arguments {
|
||||
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
|
||||
self.cell_paths.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FormatBits;
|
||||
|
||||
impl Command for FormatBits {
|
||||
fn name(&self) -> &str {
|
||||
"format bits"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("format bits")
|
||||
.input_output_types(vec![
|
||||
(Type::Binary, Type::String),
|
||||
(Type::Int, Type::String),
|
||||
(Type::Filesize, Type::String),
|
||||
(Type::Duration, Type::String),
|
||||
(Type::String, Type::String),
|
||||
(Type::Bool, Type::String),
|
||||
(Type::table(), Type::table()),
|
||||
(Type::record(), Type::record()),
|
||||
])
|
||||
.allow_variants_without_examples(true) // TODO: supply exhaustive examples
|
||||
.rest(
|
||||
"rest",
|
||||
SyntaxShape::CellPath,
|
||||
"for a data structure input, convert data at the given cell paths",
|
||||
)
|
||||
.category(Category::Conversions)
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Convert value to a string of binary data represented by 0 and 1."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["convert", "cast", "binary"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
format_bits(engine_state, stack, call, input)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "convert a binary value into a string, padded to 8 places with 0s",
|
||||
example: "0x[1] | format bits",
|
||||
result: Some(Value::string("00000001",
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "convert an int into a string, padded to 8 places with 0s",
|
||||
example: "1 | format bits",
|
||||
result: Some(Value::string("00000001",
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "convert a filesize value into a string, padded to 8 places with 0s",
|
||||
example: "1b | format bits",
|
||||
result: Some(Value::string("00000001",
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "convert a duration value into a string, padded to 8 places with 0s",
|
||||
example: "1ns | format bits",
|
||||
result: Some(Value::string("00000001",
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "convert a boolean value into a string, padded to 8 places with 0s",
|
||||
example: "true | format bits",
|
||||
result: Some(Value::string("00000001",
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "convert a string into a raw binary string, padded with 0s to 8 places",
|
||||
example: "'nushell.sh' | format bits",
|
||||
result: Some(Value::string("01101110 01110101 01110011 01101000 01100101 01101100 01101100 00101110 01110011 01101000",
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: crate public only during deprecation
|
||||
pub(crate) fn format_bits(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let cell_paths = call.rest(engine_state, stack, 0)?;
|
||||
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||
|
||||
if let PipelineData::ByteStream(stream, metadata) = input {
|
||||
Ok(PipelineData::ByteStream(
|
||||
byte_stream_to_bits(stream, head),
|
||||
metadata,
|
||||
))
|
||||
} else {
|
||||
let args = Arguments { cell_paths };
|
||||
operate(action, args, input, call.head, engine_state.signals())
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_stream_to_bits(stream: ByteStream, head: Span) -> ByteStream {
|
||||
if let Some(mut reader) = stream.reader() {
|
||||
let mut is_first = true;
|
||||
ByteStream::from_fn(
|
||||
head,
|
||||
Signals::empty(),
|
||||
ByteStreamType::String,
|
||||
move |buffer| {
|
||||
let mut byte = [0];
|
||||
if reader
|
||||
.read(&mut byte[..])
|
||||
.map_err(|err| IoError::new(err.kind(), head, None))?
|
||||
> 0
|
||||
{
|
||||
// Format the byte as bits
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else {
|
||||
buffer.push(b' ');
|
||||
}
|
||||
write!(buffer, "{:08b}", byte[0]).expect("format failed");
|
||||
Ok(true)
|
||||
} else {
|
||||
// EOF
|
||||
Ok(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
ByteStream::read(io::empty(), head, Signals::empty(), ByteStreamType::String)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_to_smallest_number_type(num: i64, span: Span) -> Value {
|
||||
if let Some(v) = num.to_i8() {
|
||||
let bytes = v.to_ne_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
} else if let Some(v) = num.to_i16() {
|
||||
let bytes = v.to_ne_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
} else if let Some(v) = num.to_i32() {
|
||||
let bytes = v.to_ne_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
} else {
|
||||
let bytes = num.to_ne_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
}
|
||||
}
|
||||
|
||||
fn action(input: &Value, _args: &Arguments, span: Span) -> Value {
|
||||
match input {
|
||||
Value::Binary { val, .. } => {
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in val {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
}
|
||||
Value::Int { 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::String { val, .. } => {
|
||||
let raw_bytes = val.as_bytes();
|
||||
let mut raw_string = "".to_string();
|
||||
for ch in raw_bytes {
|
||||
raw_string.push_str(&format!("{:08b} ", ch));
|
||||
}
|
||||
Value::string(raw_string.trim(), span)
|
||||
}
|
||||
Value::Bool { val, .. } => {
|
||||
let v = <i64 as From<bool>>::from(*val);
|
||||
convert_to_smallest_number_type(v, span)
|
||||
}
|
||||
// Propagate errors by explicitly matching them before the final case.
|
||||
Value::Error { .. } => input.clone(),
|
||||
other => Value::error(
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "int, filesize, string, duration, binary, or bool".into(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: span,
|
||||
src_span: other.span(),
|
||||
},
|
||||
span,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(FormatBits {})
|
||||
}
|
||||
}
|
@ -1,3 +1,9 @@
|
||||
mod bits;
|
||||
mod command;
|
||||
mod number;
|
||||
|
||||
pub(crate) use command::FormatPattern;
|
||||
// TODO remove `format_bits` visibility after removal of into bits
|
||||
pub(crate) use bits::{format_bits, FormatBits};
|
||||
// TODO remove `format_number` visibility after removal of into bits
|
||||
pub(crate) use number::{format_number, FormatNumber};
|
||||
|
126
crates/nu-cmd-extra/src/extra/strings/format/number.rs
Normal file
126
crates/nu-cmd-extra/src/extra/strings/format/number.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FormatNumber;
|
||||
|
||||
impl Command for FormatNumber {
|
||||
fn name(&self) -> &str {
|
||||
"format number"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Format a number."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("format number")
|
||||
.input_output_types(vec![(Type::Number, Type::record())])
|
||||
.category(Category::Conversions)
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["display", "render", "format"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Get a record containing multiple formats for the number 42",
|
||||
example: "42 | format number",
|
||||
result: Some(Value::test_record(record! {
|
||||
"binary" => Value::test_string("0b101010"),
|
||||
"debug" => Value::test_string("42"),
|
||||
"display" => Value::test_string("42"),
|
||||
"lowerexp" => Value::test_string("4.2e1"),
|
||||
"lowerhex" => Value::test_string("0x2a"),
|
||||
"octal" => Value::test_string("0o52"),
|
||||
"upperexp" => Value::test_string("4.2E1"),
|
||||
"upperhex" => Value::test_string("0x2A"),
|
||||
})),
|
||||
}]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
format_number(engine_state, stack, call, input)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_number(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
|
||||
let args = CellPathOnlyArgs::from(cell_paths);
|
||||
operate(action, args, input, call.head, engine_state.signals())
|
||||
}
|
||||
|
||||
fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
|
||||
match input {
|
||||
Value::Float { val, .. } => format_f64(*val, span),
|
||||
Value::Int { val, .. } => format_i64(*val, span),
|
||||
Value::Filesize { val, .. } => format_i64(val.get(), span),
|
||||
// Propagate errors by explicitly matching them before the final case.
|
||||
Value::Error { .. } => input.clone(),
|
||||
other => Value::error(
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "float, int, or filesize".into(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: span,
|
||||
src_span: other.span(),
|
||||
},
|
||||
span,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_i64(num: i64, span: Span) -> Value {
|
||||
Value::record(
|
||||
record! {
|
||||
"binary" => Value::string(format!("{num:#b}"), span),
|
||||
"debug" => Value::string(format!("{num:#?}"), span),
|
||||
"display" => Value::string(format!("{num}"), span),
|
||||
"lowerexp" => Value::string(format!("{num:#e}"), span),
|
||||
"lowerhex" => Value::string(format!("{num:#x}"), span),
|
||||
"octal" => Value::string(format!("{num:#o}"), span),
|
||||
"upperexp" => Value::string(format!("{num:#E}"), span),
|
||||
"upperhex" => Value::string(format!("{num:#X}"), span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
fn format_f64(num: f64, span: Span) -> Value {
|
||||
Value::record(
|
||||
record! {
|
||||
"binary" => Value::string(format!("{:b}", num.to_bits()), span),
|
||||
"debug" => Value::string(format!("{num:#?}"), span),
|
||||
"display" => Value::string(format!("{num}"), span),
|
||||
"lowerexp" => Value::string(format!("{num:#e}"), span),
|
||||
"lowerhex" => Value::string(format!("{:0x}", num.to_bits()), span),
|
||||
"octal" => Value::string(format!("{:0o}", num.to_bits()), span),
|
||||
"upperexp" => Value::string(format!("{num:#E}"), span),
|
||||
"upperhex" => Value::string(format!("{:0X}", num.to_bits()), span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(FormatNumber {})
|
||||
}
|
||||
}
|
@ -2,12 +2,12 @@ use nu_test_support::nu;
|
||||
|
||||
#[test]
|
||||
fn byte_stream_into_bits() {
|
||||
let result = nu!("[0x[01] 0x[02 03]] | bytes collect | into bits");
|
||||
let result = nu!("[0x[01] 0x[02 03]] | bytes collect | format bits");
|
||||
assert_eq!("00000001 00000010 00000011", result.out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_stream_into_bits_is_stream() {
|
||||
let result = nu!("[0x[01] 0x[02 03]] | bytes collect | into bits | describe");
|
||||
let result = nu!("[0x[01] 0x[02 03]] | bytes collect | format bits | describe");
|
||||
assert_eq!("string (stream)", result.out);
|
||||
}
|
@ -1 +1 @@
|
||||
mod into;
|
||||
mod format;
|
||||
|
@ -6,7 +6,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-lang"
|
||||
version = "0.99.1"
|
||||
version = "0.102.0"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
@ -15,18 +15,29 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.99.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.102.0", default-features = false }
|
||||
nu-parser = { path = "../nu-parser", version = "0.102.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.102.0", default-features = false }
|
||||
nu-utils = { path = "../nu-utils", version = "0.102.0", default-features = false }
|
||||
|
||||
itertools = { workspace = true }
|
||||
shadow-rs = { version = "0.35", default-features = false }
|
||||
shadow-rs = { version = "0.38", default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { version = "0.35", default-features = false }
|
||||
shadow-rs = { version = "0.38", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["os"]
|
||||
os = [
|
||||
"nu-engine/os",
|
||||
"nu-protocol/os",
|
||||
"nu-utils/os",
|
||||
]
|
||||
plugin = [
|
||||
"nu-protocol/plugin",
|
||||
"os",
|
||||
]
|
||||
|
||||
mimalloc = []
|
||||
trash-support = []
|
||||
sqlite = []
|
||||
|
@ -1,12 +1,13 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() -> shadow_rs::SdResult<()> {
|
||||
fn main() {
|
||||
// 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)
|
||||
let hash = get_git_hash().unwrap_or_default();
|
||||
println!("cargo:rustc-env=NU_COMMIT_HASH={hash}");
|
||||
|
||||
shadow_rs::new()
|
||||
shadow_rs::ShadowBuilder::builder()
|
||||
.build()
|
||||
.expect("shadow builder build should success");
|
||||
}
|
||||
|
||||
fn get_git_hash() -> Option<String> {
|
||||
|
@ -169,6 +169,7 @@ fn run(
|
||||
let origin = match stream.source() {
|
||||
ByteStreamSource::Read(_) => "unknown",
|
||||
ByteStreamSource::File(_) => "file",
|
||||
#[cfg(feature = "os")]
|
||||
ByteStreamSource::Child(_) => "external",
|
||||
};
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
use nu_engine::{command_prelude::*, get_eval_block_with_early_return, redirect_env};
|
||||
#[cfg(feature = "os")]
|
||||
use nu_protocol::process::{ChildPipe, ChildProcess};
|
||||
use nu_protocol::{
|
||||
engine::Closure,
|
||||
process::{ChildPipe, ChildProcess},
|
||||
ByteStream, ByteStreamSource, OutDest,
|
||||
engine::Closure, shell_error::io::IoError, ByteStream, ByteStreamSource, OutDest,
|
||||
};
|
||||
|
||||
use std::{
|
||||
io::{Cursor, Read},
|
||||
thread,
|
||||
@ -69,6 +70,33 @@ impl Command for Do {
|
||||
let block: Closure = call.req(engine_state, caller_stack, 0)?;
|
||||
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
|
||||
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
|
||||
|| call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?;
|
||||
let ignore_program_errors = ignore_all_errors
|
||||
@ -82,9 +110,6 @@ impl Command for Do {
|
||||
bind_args_to(&mut callee_stack, &block.signature, rest, head)?;
|
||||
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);
|
||||
|
||||
if has_env {
|
||||
@ -95,6 +120,13 @@ impl Command for Do {
|
||||
match result {
|
||||
Ok(PipelineData::ByteStream(stream, metadata)) if capture_errors => {
|
||||
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() {
|
||||
Ok(mut child) => {
|
||||
// Use a thread to receive stdout message.
|
||||
@ -113,10 +145,16 @@ impl Command for Do {
|
||||
.name("stdout consumer".to_string())
|
||||
.spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
stdout.read_to_end(&mut buf)?;
|
||||
stdout.read_to_end(&mut buf).map_err(|err| {
|
||||
IoError::new_internal(
|
||||
err.kind(),
|
||||
"Could not read stdout to end",
|
||||
nu_protocol::location!(),
|
||||
)
|
||||
})?;
|
||||
Ok::<_, ShellError>(buf)
|
||||
})
|
||||
.err_span(head)
|
||||
.map_err(|err| IoError::new(err.kind(), head, None))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
@ -126,7 +164,9 @@ impl Command for Do {
|
||||
None => String::new(),
|
||||
Some(mut stderr) => {
|
||||
let mut buf = String::new();
|
||||
stderr.read_to_string(&mut buf).err_span(span)?;
|
||||
stderr
|
||||
.read_to_string(&mut buf)
|
||||
.map_err(|err| IoError::new(err.kind(), span, None))?;
|
||||
buf
|
||||
}
|
||||
};
|
||||
@ -172,6 +212,7 @@ impl Command for Do {
|
||||
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value
|
||||
) =>
|
||||
{
|
||||
#[cfg(feature = "os")]
|
||||
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
||||
child.ignore_error(true);
|
||||
}
|
||||
@ -211,16 +252,6 @@ impl Command for Do {
|
||||
example: r#"do --ignore-errors { thisisnotarealcommand }"#,
|
||||
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 {
|
||||
description: "Abort the pipeline if a program returns a non-zero exit code",
|
||||
example: r#"do --capture-errors { nu --commands 'exit 1' } | myscarycommand"#,
|
||||
@ -273,22 +304,13 @@ fn bind_args_to(
|
||||
.expect("internal error: all custom parameters must have var_ids");
|
||||
if let Some(result) = val_iter.next() {
|
||||
let param_type = param.shape.to_type();
|
||||
if required && !result.get_type().is_subtype(¶m_type) {
|
||||
// need to check if result is an empty list, and param_type is table or list
|
||||
// nushell needs to pass type checking for the case.
|
||||
let empty_list_matches = result
|
||||
.as_list()
|
||||
.map(|l| l.is_empty() && matches!(param_type, Type::List(_) | Type::Table(_)))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !empty_list_matches {
|
||||
return Err(ShellError::CantConvert {
|
||||
to_type: param.shape.to_type().to_string(),
|
||||
from_type: result.get_type().to_string(),
|
||||
span: result.span(),
|
||||
help: None,
|
||||
});
|
||||
}
|
||||
if required && !result.is_subtype_of(¶m_type) {
|
||||
return Err(ShellError::CantConvert {
|
||||
to_type: param.shape.to_type().to_string(),
|
||||
from_type: result.get_type().to_string(),
|
||||
span: result.span(),
|
||||
help: None,
|
||||
});
|
||||
}
|
||||
stack.add_var(var_id, result);
|
||||
} else if let Some(value) = ¶m.default_value {
|
||||
|
@ -35,6 +35,7 @@ impl Command for Ignore {
|
||||
mut input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if let PipelineData::ByteStream(stream, _) = &mut input {
|
||||
#[cfg(feature = "os")]
|
||||
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
||||
child.ignore_error(true);
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ impl Command for OverlayUse {
|
||||
.allow_variants_without_examples(true)
|
||||
.required(
|
||||
"name",
|
||||
SyntaxShape::String,
|
||||
"Module name to use overlay for.",
|
||||
SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]),
|
||||
"Module name to use overlay for (`null` for no-op).",
|
||||
)
|
||||
.optional(
|
||||
"as",
|
||||
@ -61,6 +61,11 @@ impl Command for OverlayUse {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let noop = call.get_parser_info(caller_stack, "noop");
|
||||
if noop.is_some() {
|
||||
return Ok(PipelineData::empty());
|
||||
}
|
||||
|
||||
let mut name_arg: Spanned<String> = call.req(engine_state, caller_stack, 0)?;
|
||||
name_arg.item = trim_quotes_str(&name_arg.item).to_string();
|
||||
|
||||
|
@ -107,7 +107,7 @@ fn run_catch(
|
||||
|
||||
if let Some(catch) = catch {
|
||||
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);
|
||||
// Put the error value in the positional closure var
|
||||
if let Some(var) = block.signature.get_positional(0) {
|
||||
|
@ -22,7 +22,11 @@ impl Command for Use {
|
||||
Signature::build("use")
|
||||
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||
.allow_variants_without_examples(true)
|
||||
.required("module", SyntaxShape::String, "Module or module file.")
|
||||
.required(
|
||||
"module",
|
||||
SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]),
|
||||
"Module or module file (`null` for no-op).",
|
||||
)
|
||||
.rest(
|
||||
"members",
|
||||
SyntaxShape::Any,
|
||||
@ -54,6 +58,9 @@ This command is a parser keyword. For details, check:
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if call.get_parser_info(caller_stack, "noop").is_some() {
|
||||
return Ok(PipelineData::empty());
|
||||
}
|
||||
let Some(Expression {
|
||||
expr: Expr::ImportPattern(import_pattern),
|
||||
..
|
||||
@ -98,15 +105,21 @@ This command is a parser keyword. For details, check:
|
||||
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,
|
||||
engine_state,
|
||||
caller_stack,
|
||||
get_dirs_var_from_call(caller_stack, call),
|
||||
)?;
|
||||
let maybe_parent = maybe_file_path
|
||||
.as_ref()
|
||||
.and_then(|path| path.parent().map(|p| p.to_path_buf()));
|
||||
// module_arg_str maybe a directory, in this case
|
||||
// find_in_dirs_env returns a directory.
|
||||
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
|
||||
.gather_captures(engine_state, &block.captures)
|
||||
@ -118,9 +131,15 @@ This command is a parser keyword. For details, check:
|
||||
callee_stack.add_env_var("FILE_PWD".to_string(), file_pwd);
|
||||
}
|
||||
|
||||
if let Some(file_path) = maybe_file_path {
|
||||
let file_path = Value::string(file_path.to_string_lossy(), call.head);
|
||||
callee_stack.add_env_var("CURRENT_FILE".to_string(), file_path);
|
||||
if let Some(path) = maybe_file_path_or_dir {
|
||||
let module_file_path = if path.is_dir() {
|
||||
// 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);
|
||||
|
@ -116,24 +116,30 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
|
||||
Value::string(features_enabled().join(", "), span),
|
||||
);
|
||||
|
||||
// Get a list of plugin names and versions if present
|
||||
let installed_plugins = engine_state
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let name = x.identity().name();
|
||||
if let Some(version) = x.metadata().and_then(|m| m.version) {
|
||||
format!("{name} {version}")
|
||||
} else {
|
||||
name.into()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
#[cfg(not(feature = "plugin"))]
|
||||
let _ = engine_state;
|
||||
|
||||
record.push(
|
||||
"installed_plugins",
|
||||
Value::string(installed_plugins.join(", "), span),
|
||||
);
|
||||
#[cfg(feature = "plugin")]
|
||||
{
|
||||
// Get a list of plugin names and versions if present
|
||||
let installed_plugins = engine_state
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let name = x.identity().name();
|
||||
if let Some(version) = x.metadata().and_then(|m| m.version) {
|
||||
format!("{name} {version}")
|
||||
} else {
|
||||
name.into()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
record.push(
|
||||
"installed_plugins",
|
||||
Value::string(installed_plugins.join(", "), span),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Value::record(record, span).into_pipeline_data())
|
||||
}
|
||||
|
@ -19,18 +19,15 @@ pub fn check_example_input_and_output_types_match_command_signature(
|
||||
|
||||
// Skip tests that don't have results to compare to
|
||||
if let Some(example_output) = example.result.as_ref() {
|
||||
if let Some(example_input_type) =
|
||||
if let Some(example_input) =
|
||||
eval_pipeline_without_terminal_expression(example.example, cwd, engine_state)
|
||||
{
|
||||
let example_input_type = example_input_type.get_type();
|
||||
let example_output_type = example_output.get_type();
|
||||
|
||||
let example_matches_signature =
|
||||
signature_input_output_types
|
||||
.iter()
|
||||
.any(|(sig_in_type, sig_out_type)| {
|
||||
example_input_type.is_subtype(sig_in_type)
|
||||
&& example_output_type.is_subtype(sig_out_type)
|
||||
example_input.is_subtype_of(sig_in_type)
|
||||
&& example_output.is_subtype_of(sig_out_type)
|
||||
&& {
|
||||
witnessed_type_transformations
|
||||
.insert((sig_in_type.clone(), sig_out_type.clone()));
|
||||
@ -38,6 +35,9 @@ pub fn check_example_input_and_output_types_match_command_signature(
|
||||
}
|
||||
});
|
||||
|
||||
let example_input_type = example_input.get_type();
|
||||
let example_output_type = example_output.get_type();
|
||||
|
||||
// The example type checks as a cell path operation if both:
|
||||
// 1. The command is declared to operate on cell paths.
|
||||
// 2. The example_input_type is list or record or table, and the example
|
||||
@ -139,6 +139,7 @@ pub fn eval_block(
|
||||
}
|
||||
|
||||
pub fn check_example_evaluates_to_expected_output(
|
||||
cmd_name: &str,
|
||||
example: &Example,
|
||||
cwd: &std::path::Path,
|
||||
engine_state: &mut Box<EngineState>,
|
||||
@ -159,11 +160,17 @@ pub fn check_example_evaluates_to_expected_output(
|
||||
// If the command you are testing requires to compare another case, then
|
||||
// you need to define its equality in the Value struct
|
||||
if let Some(expected) = example.result.as_ref() {
|
||||
let expected = DebuggableValue(expected);
|
||||
let result = DebuggableValue(&result);
|
||||
assert_eq!(
|
||||
DebuggableValue(&result),
|
||||
DebuggableValue(expected),
|
||||
"The example result differs from the expected value",
|
||||
)
|
||||
result,
|
||||
expected,
|
||||
"Error: The result of example '{}' for the command '{}' differs from the expected value.\n\nExpected: {:?}\nActual: {:?}\n",
|
||||
example.description,
|
||||
cmd_name,
|
||||
expected,
|
||||
result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,12 @@ mod test_examples {
|
||||
signature.operates_on_cell_paths(),
|
||||
),
|
||||
);
|
||||
check_example_evaluates_to_expected_output(&example, cwd.as_path(), &mut engine_state);
|
||||
check_example_evaluates_to_expected_output(
|
||||
cmd.name(),
|
||||
&example,
|
||||
cwd.as_path(),
|
||||
&mut engine_state,
|
||||
);
|
||||
}
|
||||
|
||||
check_all_signature_input_output_types_entries_have_examples(
|
||||
|
@ -1,3 +1,4 @@
|
||||
#![cfg_attr(not(feature = "os"), allow(unused))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
mod core_commands;
|
||||
mod default_context;
|
||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-plugin"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
|
||||
version = "0.99.1"
|
||||
version = "0.102.0"
|
||||
|
||||
# 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
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
||||
nu-path = { path = "../nu-path", version = "0.99.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1", features = ["plugin"] }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.99.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.102.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.102.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.102.0", features = ["plugin"] }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.102.0" }
|
||||
|
||||
itertools = { workspace = true }
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
use crate::util::{get_plugin_dirs, modify_plugin_file};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_plugin_engine::{GetPlugin, PersistentPlugin};
|
||||
use nu_protocol::{PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin};
|
||||
use std::sync::Arc;
|
||||
use nu_protocol::{
|
||||
shell_error::io::IoError, PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin,
|
||||
};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginAdd;
|
||||
@ -86,11 +88,14 @@ apparent the next time `nu` is next launched with that plugin registry file.
|
||||
let filename_expanded = nu_path::locate_in_dirs(&filename.item, &cwd, || {
|
||||
get_plugin_dirs(engine_state, stack)
|
||||
})
|
||||
.err_span(filename.span)?;
|
||||
.map_err(|err| IoError::new(err.kind(), filename.span, PathBuf::from(filename.item)))?;
|
||||
|
||||
let shell_expanded = shell
|
||||
.as_ref()
|
||||
.map(|s| nu_path::canonicalize_with(&s.item, &cwd).err_span(s.span))
|
||||
.map(|s| {
|
||||
nu_path::canonicalize_with(&s.item, &cwd)
|
||||
.map_err(|err| IoError::new(err.kind(), s.span, None))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Parse the plugin filename so it can be used to spawn the plugin
|
||||
@ -119,7 +124,7 @@ apparent the next time `nu` is next launched with that plugin registry file.
|
||||
let metadata = interface.get_metadata()?;
|
||||
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
|
||||
let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
|
||||
contents.upsert_plugin(item);
|
||||
|
@ -1,5 +1,8 @@
|
||||
use itertools::Itertools;
|
||||
use itertools::{EitherOrBoth, Itertools};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{IntoValue, PluginRegistryItemData};
|
||||
|
||||
use crate::util::read_plugin_file;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginList;
|
||||
@ -17,7 +20,7 @@ impl Command for PluginList {
|
||||
[
|
||||
("name".into(), Type::String),
|
||||
("version".into(), Type::String),
|
||||
("is_running".into(), Type::Bool),
|
||||
("status".into(), Type::String),
|
||||
("pid".into(), Type::Int),
|
||||
("filename".into(), Type::String),
|
||||
("shell".into(), Type::String),
|
||||
@ -26,11 +29,54 @@ impl Command for PluginList {
|
||||
.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)
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -45,7 +91,7 @@ impl Command for PluginList {
|
||||
result: Some(Value::test_list(vec![Value::test_record(record! {
|
||||
"name" => Value::test_string("inc"),
|
||||
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
|
||||
"is_running" => Value::test_bool(true),
|
||||
"status" => Value::test_string("running"),
|
||||
"pid" => Value::test_int(106480),
|
||||
"filename" => if cfg!(windows) {
|
||||
Value::test_string(r"C:\nu\plugins\nu_plugin_inc.exe")
|
||||
@ -67,58 +113,189 @@ impl Command for PluginList {
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> 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")?;
|
||||
|
||||
// Group plugin decls by plugin identity
|
||||
let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
|
||||
decl.plugin_identity()
|
||||
.expect("plugin decl should have identity")
|
||||
});
|
||||
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)?,
|
||||
};
|
||||
|
||||
// Build plugins list
|
||||
let list = engine_state.plugins().iter().map(|plugin| {
|
||||
// Find commands that belong to the plugin
|
||||
let commands = decls.get(plugin.identity())
|
||||
.into_iter()
|
||||
.flat_map(|decls| {
|
||||
decls.iter().map(|decl| Value::string(decl.name(), head))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let pid = plugin
|
||||
.pid()
|
||||
.map(|p| Value::int(p as i64, head))
|
||||
.unwrap_or(Value::nothing(head));
|
||||
|
||||
let shell = plugin
|
||||
.identity()
|
||||
.shell()
|
||||
.map(|s| Value::string(s.to_string_lossy(), head))
|
||||
.unwrap_or(Value::nothing(head));
|
||||
|
||||
let metadata = plugin.metadata();
|
||||
let version = metadata
|
||||
.and_then(|m| m.version)
|
||||
.map(|s| Value::string(s, head))
|
||||
.unwrap_or(Value::nothing(head));
|
||||
|
||||
let record = record! {
|
||||
"name" => Value::string(plugin.identity().name(), head),
|
||||
"version" => version,
|
||||
"is_running" => Value::bool(plugin.is_running(), head),
|
||||
"pid" => pid,
|
||||
"filename" => Value::string(plugin.identity().filename().to_string_lossy(), head),
|
||||
"shell" => shell,
|
||||
"commands" => Value::list(commands, head),
|
||||
};
|
||||
|
||||
Value::record(record, head)
|
||||
}).collect();
|
||||
|
||||
Ok(Value::list(list, head).into_pipeline_data())
|
||||
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
|
||||
let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
|
||||
decl.plugin_identity()
|
||||
.expect("plugin decl should have identity")
|
||||
});
|
||||
|
||||
// Build plugins list
|
||||
engine_state
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|plugin| {
|
||||
// Find commands that belong to the plugin
|
||||
let commands = decls
|
||||
.get(plugin.identity())
|
||||
.into_iter()
|
||||
.flat_map(|decls| decls.iter().map(|decl| decl.name().to_owned()))
|
||||
.sorted()
|
||||
.collect();
|
||||
|
||||
PluginInfo {
|
||||
name: plugin.identity().name().into(),
|
||||
version: plugin.metadata().and_then(|m| m.version),
|
||||
status: if plugin.pid().is_some() {
|
||||
PluginStatus::Running
|
||||
} else {
|
||||
PluginStatus::Loaded
|
||||
},
|
||||
pid: plugin.pid(),
|
||||
filename: plugin.identity().filename().to_string_lossy().into_owned(),
|
||||
shell: plugin
|
||||
.identity()
|
||||
.shell()
|
||||
.map(|path| path.to_string_lossy().into_owned()),
|
||||
commands,
|
||||
}
|
||||
})
|
||||
.sorted()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_plugins_in_registry(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
span: Span,
|
||||
custom_path: &Option<Spanned<String>>,
|
||||
) -> Result<Vec<PluginInfo>, ShellError> {
|
||||
let plugin_file_contents = read_plugin_file(engine_state, stack, span, custom_path)?;
|
||||
|
||||
let plugins_info = plugin_file_contents
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| {
|
||||
let mut info = PluginInfo {
|
||||
name: plugin.name,
|
||||
version: None,
|
||||
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![],
|
||||
};
|
||||
|
||||
if let PluginRegistryItemData::Valid { metadata, commands } = plugin.data {
|
||||
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(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);
|
||||
|
||||
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
|
||||
.plugins
|
||||
.iter()
|
||||
|
@ -1,23 +1,22 @@
|
||||
#[allow(deprecated)]
|
||||
use nu_engine::{command_prelude::*, current_dir};
|
||||
use nu_protocol::{engine::StateWorkingSet, PluginRegistryFile};
|
||||
use nu_protocol::{engine::StateWorkingSet, shell_error::io::IoError, PluginRegistryFile};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
pub(crate) fn modify_plugin_file(
|
||||
fn get_plugin_registry_file_path(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
span: Span,
|
||||
custom_path: Option<Spanned<String>>,
|
||||
operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
custom_path: &Option<Spanned<String>>,
|
||||
) -> Result<PathBuf, ShellError> {
|
||||
#[allow(deprecated)]
|
||||
let cwd = current_dir(engine_state, stack)?;
|
||||
|
||||
let plugin_registry_file_path = if let Some(ref custom_path) = custom_path {
|
||||
nu_path::expand_path_with(&custom_path.item, cwd, true)
|
||||
if let Some(ref custom_path) = custom_path {
|
||||
Ok(nu_path::expand_path_with(&custom_path.item, cwd, true))
|
||||
} else {
|
||||
engine_state
|
||||
.plugin_path
|
||||
@ -28,21 +27,56 @@ pub(crate) fn modify_plugin_file(
|
||||
span: Some(span),
|
||||
help: Some("you may be running `nu` with --no-config-file".into()),
|
||||
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| IoError::new(err.kind(), file_span, plugin_registry_file_path))?,
|
||||
Some(file_span),
|
||||
)
|
||||
} else if let Some(path) = custom_path {
|
||||
Err(ShellError::Io(IoError::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
path.span,
|
||||
PathBuf::from(&path.item),
|
||||
)))
|
||||
} 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);
|
||||
|
||||
// Try to read the plugin file if it exists
|
||||
let mut contents = if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) {
|
||||
PluginRegistryFile::read_from(
|
||||
File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
|
||||
msg: format!(
|
||||
"failed to read `{}`: {}",
|
||||
plugin_registry_file_path.display(),
|
||||
err
|
||||
),
|
||||
span: file_span,
|
||||
File::open(&plugin_registry_file_path).map_err(|err| {
|
||||
IoError::new(err.kind(), file_span, plugin_registry_file_path.clone())
|
||||
})?,
|
||||
Some(file_span),
|
||||
)?
|
||||
@ -55,14 +89,8 @@ pub(crate) fn modify_plugin_file(
|
||||
|
||||
// Save the modified file on success
|
||||
contents.write_to(
|
||||
File::create(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
|
||||
msg: format!(
|
||||
"failed to create `{}`: {}",
|
||||
plugin_registry_file_path.display(),
|
||||
err
|
||||
),
|
||||
span: file_span,
|
||||
})?,
|
||||
File::create(&plugin_registry_file_path)
|
||||
.map_err(|err| IoError::new(err.kind(), file_span, plugin_registry_file_path))?,
|
||||
Some(span),
|
||||
)?;
|
||||
|
||||
@ -91,18 +119,24 @@ pub(crate) fn get_plugin_dirs(
|
||||
engine_state: &EngineState,
|
||||
stack: &Stack,
|
||||
) -> 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 value = working_set
|
||||
let dirs_from_const = working_set
|
||||
.find_variable(b"$NU_PLUGIN_DIRS")
|
||||
.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
|
||||
|
||||
// Get all of the strings in the list, if possible
|
||||
value
|
||||
.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())
|
||||
.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"
|
||||
license = "MIT"
|
||||
name = "nu-color-config"
|
||||
version = "0.99.1"
|
||||
version = "0.102.0"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
@ -14,12 +14,12 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
||||
nu-json = { path = "../nu-json", version = "0.99.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.102.0", default-features = false }
|
||||
nu-engine = { path = "../nu-engine", version = "0.102.0", default-features = false }
|
||||
nu-json = { path = "../nu-json", version = "0.102.0" }
|
||||
nu-ansi-term = { workspace = true }
|
||||
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.99.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.102.0" }
|
@ -5,7 +5,6 @@ use nu_protocol::{Config, Value};
|
||||
// The default colors for shapes, used when there is no config for them.
|
||||
pub fn default_shape_color(shape: &str) -> Style {
|
||||
match shape {
|
||||
"shape_and" => Style::new().fg(Color::Purple).bold(),
|
||||
"shape_binary" => Style::new().fg(Color::Purple).bold(),
|
||||
"shape_block" => Style::new().fg(Color::Blue).bold(),
|
||||
"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_nothing" => Style::new().fg(Color::LightCyan),
|
||||
"shape_operator" => Style::new().fg(Color::Yellow),
|
||||
"shape_or" => Style::new().fg(Color::Purple).bold(),
|
||||
"shape_pipe" => Style::new().fg(Color::Purple).bold(),
|
||||
"shape_range" => Style::new().fg(Color::Yellow).bold(),
|
||||
"shape_raw_string" => Style::new().fg(Color::LightMagenta).bold(),
|
||||
|
@ -169,7 +169,7 @@ impl<'a> StyleComputer<'a> {
|
||||
|
||||
// Because EngineState doesn't have Debug (Dec 2022),
|
||||
// this incomplete representation must be used.
|
||||
impl<'a> Debug for StyleComputer<'a> {
|
||||
impl Debug for StyleComputer<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.debug_struct("StyleComputer")
|
||||
.field("map", &self.map)
|
||||
@ -223,7 +223,7 @@ fn test_computable_style_closure_basic() {
|
||||
];
|
||||
let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
|
||||
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"
|
||||
name = "nu-command"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
|
||||
version = "0.99.1"
|
||||
version = "0.102.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -16,21 +16,21 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.99.1" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.99.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.99.1" }
|
||||
nu-glob = { path = "../nu-glob", version = "0.99.1" }
|
||||
nu-json = { path = "../nu-json", version = "0.99.1" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.99.1" }
|
||||
nu-path = { path = "../nu-path", version = "0.99.1" }
|
||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.99.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.99.1" }
|
||||
nu-system = { path = "../nu-system", version = "0.99.1" }
|
||||
nu-table = { path = "../nu-table", version = "0.99.1" }
|
||||
nu-term-grid = { path = "../nu-term-grid", version = "0.99.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.99.1" }
|
||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.102.0" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.102.0" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.102.0", default-features = false }
|
||||
nu-glob = { path = "../nu-glob", version = "0.102.0" }
|
||||
nu-json = { path = "../nu-json", version = "0.102.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.102.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.102.0" }
|
||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.102.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.102.0", default-features = false }
|
||||
nu-system = { path = "../nu-system", version = "0.102.0" }
|
||||
nu-table = { path = "../nu-table", version = "0.102.0" }
|
||||
nu-term-grid = { path = "../nu-term-grid", version = "0.102.0" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.102.0", default-features = false }
|
||||
nu-ansi-term = { workspace = true }
|
||||
nuon = { path = "../nuon", version = "0.99.1" }
|
||||
nuon = { path = "../nuon", version = "0.102.0" }
|
||||
|
||||
alphanumeric-sort = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
@ -43,8 +43,9 @@ chardetng = { workspace = true }
|
||||
chrono = { workspace = true, features = ["std", "unstable-locales", "clock"], default-features = false }
|
||||
chrono-humanize = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
crossterm = { workspace = true, optional = true }
|
||||
csv = { workspace = true }
|
||||
devicons = { workspace = true }
|
||||
dialoguer = { workspace = true, default-features = false, features = ["fuzzy-select"] }
|
||||
digest = { workspace = true, default-features = false }
|
||||
dtparse = { workspace = true }
|
||||
@ -61,24 +62,25 @@ lscolors = { workspace = true, default-features = false, features = ["nu-ansi-te
|
||||
md5 = { workspace = true }
|
||||
mime = { workspace = true }
|
||||
mime_guess = { workspace = true }
|
||||
multipart-rs = { workspace = true }
|
||||
native-tls = { workspace = true }
|
||||
notify-debouncer-full = { workspace = true, default-features = false }
|
||||
multipart-rs = { workspace = true, optional = true }
|
||||
native-tls = { workspace = true, optional = true }
|
||||
notify-debouncer-full = { workspace = true, default-features = false, optional = true }
|
||||
num-format = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
open = { workspace = true }
|
||||
os_pipe = { workspace = true }
|
||||
oem_cp = { workspace = true }
|
||||
open = { workspace = true, optional = true }
|
||||
os_pipe = { workspace = true, optional = true }
|
||||
pathdiff = { workspace = true }
|
||||
percent-encoding = { workspace = true }
|
||||
print-positions = { workspace = true }
|
||||
quick-xml = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
rayon = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
roxmltree = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["bundled", "backup", "chrono"], optional = true }
|
||||
rmp = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
serde_urlencoded = { workspace = true }
|
||||
@ -86,29 +88,31 @@ serde_yaml = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
tabled = { workspace = true, features = ["ansi"], default-features = false }
|
||||
terminal_size = { workspace = true }
|
||||
titlecase = { workspace = true }
|
||||
toml = { workspace = true, features = ["preserve_order"] }
|
||||
unicode-segmentation = { workspace = true }
|
||||
ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json", "native-tls"] }
|
||||
update-informer = { workspace = true, optional = true }
|
||||
ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json", "native-tls"], optional = true }
|
||||
url = { workspace = true }
|
||||
uu_cp = { workspace = true }
|
||||
uu_mkdir = { workspace = true }
|
||||
uu_mktemp = { workspace = true }
|
||||
uu_mv = { workspace = true }
|
||||
uu_uname = { workspace = true }
|
||||
uu_whoami = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
uu_cp = { workspace = true, optional = true }
|
||||
uu_mkdir = { workspace = true, optional = true }
|
||||
uu_mktemp = { workspace = true, optional = true }
|
||||
uu_mv = { workspace = true, optional = true }
|
||||
uu_touch = { workspace = true, optional = true }
|
||||
uu_uname = { workspace = true, optional = true }
|
||||
uu_whoami = { workspace = true, optional = true }
|
||||
uuid = { workspace = true, features = ["v4"], optional = true }
|
||||
v_htmlescape = { workspace = true }
|
||||
wax = { workspace = true }
|
||||
which = { workspace = true }
|
||||
which = { workspace = true, optional = true }
|
||||
unicode-width = { workspace = true }
|
||||
data-encoding = { version = "2.6.0", features = ["alloc"] }
|
||||
data-encoding = { version = "2.7.0", features = ["alloc"] }
|
||||
web-time = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { workspace = true }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
[target.'cfg(all(not(windows), not(target_arch = "wasm32")))'.dependencies]
|
||||
uucore = { workspace = true, features = ["mode"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
@ -134,19 +138,67 @@ features = [
|
||||
workspace = true
|
||||
|
||||
[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",
|
||||
"update-informer/native-tls",
|
||||
"ureq",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
plugin = [
|
||||
"nu-parser/plugin",
|
||||
"os",
|
||||
]
|
||||
sqlite = ["rusqlite"]
|
||||
trash-support = ["trash"]
|
||||
|
||||
[dev-dependencies]
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.99.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.99.1" }
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.102.0" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.102.0" }
|
||||
|
||||
dirs = { workspace = true }
|
||||
mockito = { workspace = true, default-features = false }
|
||||
quickcheck = { workspace = true }
|
||||
quickcheck_macros = { workspace = true }
|
||||
rstest = { workspace = true, default-features = false }
|
||||
rstest_reuse = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
|
@ -1,15 +1,14 @@
|
||||
use nu_cmd_base::{
|
||||
input_handler::{operate, CmdArgument},
|
||||
util,
|
||||
};
|
||||
use std::ops::Bound;
|
||||
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::Range;
|
||||
use nu_protocol::{IntRange, Range};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BytesAt;
|
||||
|
||||
struct Arguments {
|
||||
indexes: Subbytes,
|
||||
range: IntRange,
|
||||
cell_paths: Option<Vec<CellPath>>,
|
||||
}
|
||||
|
||||
@ -19,15 +18,6 @@ impl CmdArgument for Arguments {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(isize, isize)> for Subbytes {
|
||||
fn from(input: (isize, isize)) -> Self {
|
||||
Self(input.0, input.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Subbytes(isize, isize);
|
||||
|
||||
impl Command for BytesAt {
|
||||
fn name(&self) -> &str {
|
||||
"bytes at"
|
||||
@ -44,6 +34,7 @@ impl Command for BytesAt {
|
||||
(Type::table(), Type::table()),
|
||||
(Type::record(), Type::record()),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.required("range", SyntaxShape::Range, "The range to get bytes.")
|
||||
.rest(
|
||||
"rest",
|
||||
@ -68,86 +59,116 @@ impl Command for BytesAt {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let range: Range = call.req(engine_state, stack, 0)?;
|
||||
let indexes = match util::process_range(&range) {
|
||||
Ok(idxs) => idxs.into(),
|
||||
Err(processing_error) => {
|
||||
return Err(processing_error("could not perform subbytes", call.head));
|
||||
let range = match call.req(engine_state, stack, 0)? {
|
||||
Range::IntRange(range) => range,
|
||||
_ => {
|
||||
return Err(ShellError::UnsupportedInput {
|
||||
msg: "Float ranges are not supported for byte streams".into(),
|
||||
input: "value originates from here".into(),
|
||||
msg_span: call.head,
|
||||
input_span: call.head,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
|
||||
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||
let args = Arguments {
|
||||
indexes,
|
||||
cell_paths,
|
||||
};
|
||||
|
||||
operate(action, args, input, call.head, engine_state.signals())
|
||||
if let PipelineData::ByteStream(stream, metadata) = input {
|
||||
let stream = stream.slice(call.head, call.arguments_span(), range)?;
|
||||
Ok(PipelineData::ByteStream(stream, metadata))
|
||||
} else {
|
||||
operate(
|
||||
map_value,
|
||||
Arguments { range, cell_paths },
|
||||
input,
|
||||
call.head,
|
||||
engine_state.signals(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Get a subbytes `0x[10 01]` from the bytes `0x[33 44 55 10 01 13]`",
|
||||
example: " 0x[33 44 55 10 01 13] | bytes at 3..<4",
|
||||
result: Some(Value::test_binary(vec![0x10])),
|
||||
},
|
||||
Example {
|
||||
description: "Get a subbytes `0x[10 01 13]` from the bytes `0x[33 44 55 10 01 13]`",
|
||||
example: " 0x[33 44 55 10 01 13] | bytes at 3..6",
|
||||
result: Some(Value::test_binary(vec![0x10, 0x01, 0x13])),
|
||||
},
|
||||
Example {
|
||||
description: "Get the remaining characters from a starting index",
|
||||
example: " { data: 0x[33 44 55 10 01 13] } | bytes at 3.. data",
|
||||
description: "Extract bytes starting from a specific index",
|
||||
example: "{ data: 0x[33 44 55 10 01 13 10] } | bytes at 3.. data",
|
||||
result: Some(Value::test_record(record! {
|
||||
"data" => Value::test_binary(vec![0x10, 0x01, 0x13]),
|
||||
"data" => Value::test_binary(vec![0x10, 0x01, 0x13, 0x10]),
|
||||
})),
|
||||
},
|
||||
Example {
|
||||
description: "Get the characters from the beginning until ending index",
|
||||
example: " 0x[33 44 55 10 01 13] | bytes at ..<4",
|
||||
description: "Slice out `0x[10 01 13]` from `0x[33 44 55 10 01 13]`",
|
||||
example: "0x[33 44 55 10 01 13] | bytes at 3..5",
|
||||
result: Some(Value::test_binary(vec![0x10, 0x01, 0x13])),
|
||||
},
|
||||
Example {
|
||||
description: "Extract bytes from the start up to a specific index",
|
||||
example: "0x[33 44 55 10 01 13 10] | bytes at ..4",
|
||||
result: Some(Value::test_binary(vec![0x33, 0x44, 0x55, 0x10, 0x01])),
|
||||
},
|
||||
Example {
|
||||
description: "Extract byte `0x[10]` using an exclusive end index",
|
||||
example: "0x[33 44 55 10 01 13 10] | bytes at 3..<4",
|
||||
result: Some(Value::test_binary(vec![0x10])),
|
||||
},
|
||||
Example {
|
||||
description: "Extract bytes up to a negative index (inclusive)",
|
||||
example: "0x[33 44 55 10 01 13 10] | bytes at ..-4",
|
||||
result: Some(Value::test_binary(vec![0x33, 0x44, 0x55, 0x10])),
|
||||
},
|
||||
Example {
|
||||
description:
|
||||
"Or the characters from the beginning until ending index inside a table",
|
||||
example: r#" [[ColA ColB ColC]; [0x[11 12 13] 0x[14 15 16] 0x[17 18 19]]] | bytes at 1.. ColB ColC"#,
|
||||
description: "Slice bytes across multiple table columns",
|
||||
example: r#"[[ColA ColB ColC]; [0x[11 12 13] 0x[14 15 16] 0x[17 18 19]]] | bytes at 1.. ColB ColC"#,
|
||||
result: Some(Value::test_list(vec![Value::test_record(record! {
|
||||
"ColA" => Value::test_binary(vec![0x11, 0x12, 0x13]),
|
||||
"ColB" => Value::test_binary(vec![0x15, 0x16]),
|
||||
"ColC" => Value::test_binary(vec![0x18, 0x19]),
|
||||
})])),
|
||||
},
|
||||
Example {
|
||||
description: "Extract the last three bytes using a negative start index",
|
||||
example: "0x[33 44 55 10 01 13 10] | bytes at (-3)..",
|
||||
result: Some(Value::test_binary(vec![0x01, 0x13, 0x10])),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
let range = &args.indexes;
|
||||
fn map_value(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
let range = &args.range;
|
||||
match input {
|
||||
Value::Binary { val, .. } => {
|
||||
let len = val.len() as isize;
|
||||
let start = if range.0 < 0 { range.0 + len } else { range.0 };
|
||||
let end = if range.1 < 0 { range.1 + len } else { range.1 };
|
||||
let len = val.len() as u64;
|
||||
let start: u64 = range.absolute_start(len);
|
||||
let _start: usize = match start.try_into() {
|
||||
Ok(start) => start,
|
||||
Err(_) => {
|
||||
let span = input.span();
|
||||
return Value::error(
|
||||
ShellError::UnsupportedInput {
|
||||
msg: format!(
|
||||
"Absolute start position {start} was too large for your system arch."
|
||||
),
|
||||
input: args.range.to_string(),
|
||||
msg_span: span,
|
||||
input_span: span,
|
||||
},
|
||||
head,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if start > end {
|
||||
Value::binary(vec![], head)
|
||||
} else {
|
||||
let val_iter = val.iter().skip(start as usize);
|
||||
Value::binary(
|
||||
if end == isize::MAX {
|
||||
val_iter.copied().collect::<Vec<u8>>()
|
||||
} else {
|
||||
val_iter.take((end - start + 1) as usize).copied().collect()
|
||||
},
|
||||
head,
|
||||
)
|
||||
}
|
||||
let (start, end) = range.absolute_bounds(val.len());
|
||||
let bytes: Vec<u8> = match end {
|
||||
Bound::Unbounded => val[start..].into(),
|
||||
Bound::Included(end) => val[start..=end].into(),
|
||||
Bound::Excluded(end) => val[start..end].into(),
|
||||
};
|
||||
|
||||
Value::binary(bytes, head)
|
||||
}
|
||||
Value::Error { .. } => input.clone(),
|
||||
|
||||
other => Value::error(
|
||||
ShellError::UnsupportedInput {
|
||||
msg: "Only binary values are supported".into(),
|
||||
@ -159,3 +180,14 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
test_examples(BytesAt {})
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use nu_engine::{command_prelude::*, get_eval_expression};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BytesBuild;
|
||||
@ -49,8 +49,7 @@ impl Command for BytesBuild {
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let mut output = vec![];
|
||||
let eval_expression = get_eval_expression(engine_state);
|
||||
for val in call.rest_iter_flattened(engine_state, stack, eval_expression, 0)? {
|
||||
for val in call.rest::<Value>(engine_state, stack, 0)? {
|
||||
let val_span = val.span();
|
||||
match val {
|
||||
Value::Binary { mut val, .. } => output.append(&mut val),
|
||||
|
@ -1,5 +1,6 @@
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::shell_error::io::IoError;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{self, BufRead},
|
||||
@ -76,7 +77,7 @@ impl Command for BytesEndsWith {
|
||||
Ok(&[]) => break,
|
||||
Ok(buf) => buf,
|
||||
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e.into_spanned(span).into()),
|
||||
Err(e) => return Err(IoError::new(e.kind(), span, None).into()),
|
||||
};
|
||||
let len = buf.len();
|
||||
if len >= cap {
|
||||
|
@ -9,6 +9,7 @@ mod length;
|
||||
mod remove;
|
||||
mod replace;
|
||||
mod reverse;
|
||||
mod split;
|
||||
mod starts_with;
|
||||
|
||||
pub use add::BytesAdd;
|
||||
@ -22,4 +23,5 @@ pub use length::BytesLen;
|
||||
pub use remove::BytesRemove;
|
||||
pub use replace::BytesReplace;
|
||||
pub use reverse::BytesReverse;
|
||||
pub use split::BytesSplit;
|
||||
pub use starts_with::BytesStartsWith;
|
||||
|
109
crates/nu-command/src/bytes/split.rs
Normal file
109
crates/nu-command/src/bytes/split.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BytesSplit;
|
||||
|
||||
impl Command for BytesSplit {
|
||||
fn name(&self) -> &str {
|
||||
"bytes split"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("bytes split")
|
||||
.input_output_types(vec![(Type::Binary, Type::list(Type::Binary))])
|
||||
.required(
|
||||
"separator",
|
||||
SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::String]),
|
||||
"Bytes or string that the input will be split on (must be non-empty).",
|
||||
)
|
||||
.category(Category::Bytes)
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Split input into multiple items using a separator."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["separate", "stream"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: r#"0x[66 6F 6F 20 62 61 72 20 62 61 7A 20] | bytes split 0x[20]"#,
|
||||
description: "Split a binary value using a binary separator",
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_binary("foo"),
|
||||
Value::test_binary("bar"),
|
||||
Value::test_binary("baz"),
|
||||
Value::test_binary(""),
|
||||
])),
|
||||
},
|
||||
Example {
|
||||
example: r#"0x[61 2D 2D 62 2D 2D 63] | bytes split "--""#,
|
||||
description: "Split a binary value using a string separator",
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_binary("a"),
|
||||
Value::test_binary("b"),
|
||||
Value::test_binary("c"),
|
||||
])),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let Spanned {
|
||||
item: separator,
|
||||
span,
|
||||
}: Spanned<Vec<u8>> = call.req(engine_state, stack, 0)?;
|
||||
|
||||
if separator.is_empty() {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "Separator can't be empty".into(),
|
||||
val_span: span,
|
||||
call_span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let (split_read, md) = match input {
|
||||
PipelineData::Value(Value::Binary { val, .. }, md) => (
|
||||
ByteStream::read_binary(val, head, engine_state.signals().clone()).split(separator),
|
||||
md,
|
||||
),
|
||||
PipelineData::ByteStream(stream, md) => (stream.split(separator), md),
|
||||
input => {
|
||||
let span = input.span().unwrap_or(head);
|
||||
return Err(input.unsupported_input_error("bytes", span));
|
||||
}
|
||||
};
|
||||
if let Some(split) = split_read {
|
||||
Ok(split
|
||||
.map(move |part| match part {
|
||||
Ok(val) => Value::binary(val, head),
|
||||
Err(err) => Value::error(err, head),
|
||||
})
|
||||
.into_pipeline_data_with_metadata(head, engine_state.signals().clone(), md))
|
||||
} else {
|
||||
Ok(PipelineData::empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(BytesSplit {})
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::shell_error::io::IoError;
|
||||
use std::io::Read;
|
||||
|
||||
struct Arguments {
|
||||
@ -71,7 +72,7 @@ impl Command for BytesStartsWith {
|
||||
reader
|
||||
.take(pattern.len() as u64)
|
||||
.read_to_end(&mut start)
|
||||
.err_span(span)?;
|
||||
.map_err(|err| IoError::new(err.kind(), span, None))?;
|
||||
|
||||
Ok(Value::bool(start == pattern, head).into_pipeline_data())
|
||||
} else {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use nu_protocol::{ShellError, Span, Value};
|
||||
use nu_protocol::{Filesize, ShellError, Span, Value};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// A subset of [`Value`], which is hashable.
|
||||
@ -30,7 +30,7 @@ pub enum HashableValue {
|
||||
span: Span,
|
||||
},
|
||||
Filesize {
|
||||
val: i64,
|
||||
val: Filesize,
|
||||
span: Span,
|
||||
},
|
||||
Duration {
|
||||
@ -198,7 +198,10 @@ mod test {
|
||||
(Value::int(1, span), HashableValue::Int { val: 1, span }),
|
||||
(
|
||||
Value::filesize(1, span),
|
||||
HashableValue::Filesize { val: 1, span },
|
||||
HashableValue::Filesize {
|
||||
val: 1.into(),
|
||||
span,
|
||||
},
|
||||
),
|
||||
(
|
||||
Value::duration(1, span),
|
||||
|
@ -167,7 +167,7 @@ fn fill(
|
||||
fn action(input: &Value, args: &Arguments, span: Span) -> Value {
|
||||
match input {
|
||||
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::String { val, .. } => fill_string(val, args, span),
|
||||
// Propagate errors by explicitly matching them before the final case.
|
||||
|
@ -1,7 +1,7 @@
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
pub struct Arguments {
|
||||
struct Arguments {
|
||||
cell_paths: Option<Vec<CellPath>>,
|
||||
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 {
|
||||
Value::Binary { .. } => input.clone(),
|
||||
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::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::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),
|
||||
|
@ -1,4 +1,4 @@
|
||||
use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs};
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -16,10 +16,16 @@ impl Command for SubCommand {
|
||||
(Type::Number, Type::Bool),
|
||||
(Type::String, Type::Bool),
|
||||
(Type::Bool, Type::Bool),
|
||||
(Type::Nothing, Type::Bool),
|
||||
(Type::List(Box::new(Type::Any)), Type::table()),
|
||||
(Type::table(), Type::table()),
|
||||
(Type::record(), Type::record()),
|
||||
])
|
||||
.switch(
|
||||
"relaxed",
|
||||
"Relaxes conversion to also allow null and any strings.",
|
||||
None,
|
||||
)
|
||||
.allow_variants_without_examples(true)
|
||||
.rest(
|
||||
"rest",
|
||||
@ -44,7 +50,10 @@ impl Command for SubCommand {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
into_bool(engine_state, stack, call, input)
|
||||
let relaxed = call
|
||||
.has_flag(engine_state, stack, "relaxed")
|
||||
.unwrap_or(false);
|
||||
into_bool(engine_state, stack, call, input, relaxed)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -95,22 +104,47 @@ impl Command for SubCommand {
|
||||
example: "'true' | into bool",
|
||||
result: Some(Value::test_bool(true)),
|
||||
},
|
||||
Example {
|
||||
description: "interpret a null as false",
|
||||
example: "null | into bool --relaxed",
|
||||
result: Some(Value::test_bool(false)),
|
||||
},
|
||||
Example {
|
||||
description: "interpret any non-false, non-zero string as true",
|
||||
example: "'something' | into bool --relaxed",
|
||||
result: Some(Value::test_bool(true)),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
struct IntoBoolCmdArgument {
|
||||
cell_paths: Option<Vec<CellPath>>,
|
||||
relaxed: bool,
|
||||
}
|
||||
|
||||
impl CmdArgument for IntoBoolCmdArgument {
|
||||
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
|
||||
self.cell_paths.take()
|
||||
}
|
||||
}
|
||||
|
||||
fn into_bool(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
relaxed: bool,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
|
||||
let args = CellPathOnlyArgs::from(cell_paths);
|
||||
let cell_paths = Some(call.rest(engine_state, stack, 0)?).filter(|v| !v.is_empty());
|
||||
let args = IntoBoolCmdArgument {
|
||||
cell_paths,
|
||||
relaxed,
|
||||
};
|
||||
operate(action, args, input, call.head, engine_state.signals())
|
||||
}
|
||||
|
||||
fn string_to_boolean(s: &str, span: Span) -> Result<bool, ShellError> {
|
||||
fn strict_string_to_boolean(s: &str, span: Span) -> Result<bool, ShellError> {
|
||||
match s.trim().to_ascii_lowercase().as_str() {
|
||||
"true" => Ok(true),
|
||||
"false" => Ok(false),
|
||||
@ -132,26 +166,31 @@ fn string_to_boolean(s: &str, span: Span) -> Result<bool, ShellError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
|
||||
match input {
|
||||
Value::Bool { .. } => input.clone(),
|
||||
Value::Int { val, .. } => Value::bool(*val != 0, span),
|
||||
Value::Float { val, .. } => Value::bool(val.abs() >= f64::EPSILON, span),
|
||||
Value::String { val, .. } => match string_to_boolean(val, span) {
|
||||
fn action(input: &Value, args: &IntoBoolCmdArgument, span: Span) -> Value {
|
||||
let err = || {
|
||||
Value::error(
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "bool, int, float or string".into(),
|
||||
wrong_type: input.get_type().to_string(),
|
||||
dst_span: span,
|
||||
src_span: input.span(),
|
||||
},
|
||||
span,
|
||||
)
|
||||
};
|
||||
|
||||
match (input, args.relaxed) {
|
||||
(Value::Error { .. } | Value::Bool { .. }, _) => input.clone(),
|
||||
// In strict mode is this an error, while in relaxed this is just `false`
|
||||
(Value::Nothing { .. }, false) => err(),
|
||||
(Value::String { val, .. }, false) => match strict_string_to_boolean(val, span) {
|
||||
Ok(val) => Value::bool(val, span),
|
||||
Err(error) => Value::error(error, span),
|
||||
},
|
||||
// Propagate errors by explicitly matching them before the final case.
|
||||
Value::Error { .. } => input.clone(),
|
||||
other => Value::error(
|
||||
ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "bool, int, float or string".into(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
dst_span: span,
|
||||
src_span: other.span(),
|
||||
},
|
||||
span,
|
||||
),
|
||||
_ => match input.coerce_bool() {
|
||||
Ok(val) => Value::bool(val, span),
|
||||
Err(_) => err(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,4 +204,32 @@ mod test {
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strict_handling() {
|
||||
let span = Span::test_data();
|
||||
let args = IntoBoolCmdArgument {
|
||||
cell_paths: vec![].into(),
|
||||
relaxed: false,
|
||||
};
|
||||
|
||||
assert!(action(&Value::test_nothing(), &args, span).is_error());
|
||||
assert!(action(&Value::test_string("abc"), &args, span).is_error());
|
||||
assert!(action(&Value::test_string("true"), &args, span).is_true());
|
||||
assert!(action(&Value::test_string("FALSE"), &args, span).is_false());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relaxed_handling() {
|
||||
let span = Span::test_data();
|
||||
let args = IntoBoolCmdArgument {
|
||||
cell_paths: vec![].into(),
|
||||
relaxed: true,
|
||||
};
|
||||
|
||||
assert!(action(&Value::test_nothing(), &args, span).is_false());
|
||||
assert!(action(&Value::test_string("abc"), &args, span).is_true());
|
||||
assert!(action(&Value::test_string("true"), &args, span).is_true());
|
||||
assert!(action(&Value::test_string("FALSE"), &args, span).is_false());
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ impl Command for IntoCellPath {
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("into cell-path")
|
||||
.input_output_types(vec![
|
||||
(Type::CellPath, Type::CellPath),
|
||||
(Type::Int, Type::CellPath),
|
||||
(Type::List(Box::new(Type::Any)), Type::CellPath),
|
||||
(
|
||||
@ -56,6 +57,13 @@ impl Command for IntoCellPath {
|
||||
members: vec![PathMember::test_int(5, false)],
|
||||
})),
|
||||
},
|
||||
Example {
|
||||
description: "Convert cell path into cell path",
|
||||
example: "5 | into cell-path | into cell-path",
|
||||
result: Some(Value::test_cell_path(CellPath {
|
||||
members: vec![PathMember::test_int(5, false)],
|
||||
})),
|
||||
},
|
||||
Example {
|
||||
description: "Convert string into cell path",
|
||||
example: "'some.path' | split row '.' | into cell-path",
|
||||
@ -96,7 +104,7 @@ fn into_cell_path(call: &Call, input: PipelineData) -> Result<PipelineData, Shel
|
||||
let head = call.head;
|
||||
|
||||
match input {
|
||||
PipelineData::Value(value, _) => Ok(value_to_cell_path(&value, head)?.into_pipeline_data()),
|
||||
PipelineData::Value(value, _) => Ok(value_to_cell_path(value, head)?.into_pipeline_data()),
|
||||
PipelineData::ListStream(stream, ..) => {
|
||||
let list: Vec<_> = stream.into_iter().collect();
|
||||
Ok(list_to_cell_path(&list, head)?.into_pipeline_data())
|
||||
@ -170,10 +178,11 @@ fn record_to_path_member(
|
||||
Ok(member)
|
||||
}
|
||||
|
||||
fn value_to_cell_path(value: &Value, span: Span) -> Result<Value, ShellError> {
|
||||
fn value_to_cell_path(value: Value, span: Span) -> Result<Value, ShellError> {
|
||||
match value {
|
||||
Value::Int { val, .. } => Ok(int_to_cell_path(*val, span)),
|
||||
Value::List { vals, .. } => list_to_cell_path(vals, span),
|
||||
Value::CellPath { .. } => Ok(value),
|
||||
Value::Int { val, .. } => Ok(int_to_cell_path(val, span)),
|
||||
Value::List { vals, .. } => list_to_cell_path(&vals, span),
|
||||
other => Err(ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: "int, list".into(),
|
||||
wrong_type: other.get_type().to_string(),
|
||||
@ -184,16 +193,11 @@ fn value_to_cell_path(value: &Value, span: Span) -> Result<Value, ShellError> {
|
||||
}
|
||||
|
||||
fn value_to_path_member(val: &Value, span: Span) -> Result<PathMember, ShellError> {
|
||||
let val_span = val.span();
|
||||
let member = match val {
|
||||
Value::Int {
|
||||
val,
|
||||
internal_span: span,
|
||||
} => int_to_path_member(*val, *span)?,
|
||||
Value::String {
|
||||
val,
|
||||
internal_span: span,
|
||||
} => PathMember::string(val.into(), false, *span),
|
||||
Value::Record { val, internal_span } => record_to_path_member(val, *internal_span, span)?,
|
||||
Value::Int { val, .. } => int_to_path_member(*val, val_span)?,
|
||||
Value::String { val, .. } => PathMember::string(val.into(), false, val_span),
|
||||
Value::Record { val, .. } => record_to_path_member(val, val_span, span)?,
|
||||
other => {
|
||||
return Err(ShellError::CantConvert {
|
||||
to_type: "int or string".to_string(),
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
@ -59,11 +59,17 @@ impl Command for SubCommand {
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("into datetime")
|
||||
.input_output_types(vec![
|
||||
(Type::Date, Type::Date),
|
||||
(Type::Int, Type::Date),
|
||||
(Type::String, Type::Date),
|
||||
(Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Date))),
|
||||
(Type::table(), Type::table()),
|
||||
(Type::record(), Type::record()),
|
||||
(Type::Nothing, Type::table()),
|
||||
// FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
|
||||
// which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
|
||||
// only applicable for --list flag
|
||||
(Type::Any, Type::table()),
|
||||
])
|
||||
.allow_variants_without_examples(true)
|
||||
.named(
|
||||
@ -185,11 +191,13 @@ impl Command for SubCommand {
|
||||
example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
|
||||
#[allow(clippy::inconsistent_digit_grouping)]
|
||||
result: Some(Value::date(
|
||||
DateTime::from_naive_utc_and_offset(
|
||||
NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
||||
.expect("date calculation should not fail in test"),
|
||||
*Local::now().offset(),
|
||||
),
|
||||
Local
|
||||
.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"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_timezone(Local::now().offset()),
|
||||
Span::test_data(),
|
||||
)),
|
||||
},
|
||||
@ -202,7 +210,13 @@ impl Command for SubCommand {
|
||||
},
|
||||
Example {
|
||||
description: "Convert standard (seconds) unix timestamp to a UTC datetime",
|
||||
example: "1614434140 * 1_000_000_000 | into datetime",
|
||||
example: "1614434140 | into datetime -f '%s'",
|
||||
#[allow(clippy::inconsistent_digit_grouping)]
|
||||
result: example_result_1(1614434140_000000000),
|
||||
},
|
||||
Example {
|
||||
description: "Using a datetime as input simply returns the value",
|
||||
example: "2021-02-27T13:55:40 | into datetime",
|
||||
#[allow(clippy::inconsistent_digit_grouping)]
|
||||
result: example_result_1(1614434140_000000000),
|
||||
},
|
||||
@ -265,6 +279,11 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
let timezone = &args.zone_options;
|
||||
let dateformat = &args.format_options;
|
||||
|
||||
// noop if the input is already a datetime
|
||||
if matches!(input, Value::Date { .. }) {
|
||||
return input.clone();
|
||||
}
|
||||
|
||||
// Let's try dtparse first
|
||||
if matches!(input, Value::String { .. }) && dateformat.is_none() {
|
||||
let span = input.span();
|
||||
@ -275,12 +294,13 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
if let Ok(date) = from_human_time(&input_val) {
|
||||
match 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 dt_fixed = DateTime::from_naive_utc_and_offset(
|
||||
combined,
|
||||
*Local::now().offset(),
|
||||
);
|
||||
let local_offset = *Local::now().offset();
|
||||
let dt_fixed =
|
||||
TimeZone::from_local_datetime(&local_offset, &combined)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
return Value::date(dt_fixed, span);
|
||||
}
|
||||
ParseResult::DateTime(date) => {
|
||||
@ -289,10 +309,11 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
ParseResult::Time(time) => {
|
||||
let date = Local::now().date_naive();
|
||||
let combined = date.and_time(time);
|
||||
let dt_fixed = DateTime::from_naive_utc_and_offset(
|
||||
combined,
|
||||
*Local::now().offset(),
|
||||
);
|
||||
let local_offset = *Local::now().offset();
|
||||
let dt_fixed =
|
||||
TimeZone::from_local_datetime(&local_offset, &combined)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
return Value::date(dt_fixed, span);
|
||||
}
|
||||
}
|
||||
@ -386,13 +407,15 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||
Ok(d) => Value::date ( d, head ),
|
||||
Err(reason) => {
|
||||
match NaiveDateTime::parse_from_str(val, &dt.0) {
|
||||
Ok(d) => Value::date (
|
||||
DateTime::from_naive_utc_and_offset(
|
||||
d,
|
||||
*Local::now().offset(),
|
||||
),
|
||||
head,
|
||||
),
|
||||
Ok(d) => {
|
||||
let local_offset = *Local::now().offset();
|
||||
let dt_fixed =
|
||||
TimeZone::from_local_datetime(&local_offset, &d)
|
||||
.single()
|
||||
.unwrap_or_default();
|
||||
|
||||
Value::date (dt_fixed,head)
|
||||
}
|
||||
Err(_) => {
|
||||
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()) },
|
||||
@ -503,7 +526,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
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 fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()));
|
||||
let args = Arguments {
|
||||
@ -513,12 +543,16 @@ mod tests {
|
||||
};
|
||||
let actual = action(&date_str, &args, Span::test_data());
|
||||
let expected = Value::date(
|
||||
DateTime::from_naive_utc_and_offset(
|
||||
NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P").unwrap(),
|
||||
*Local::now().offset(),
|
||||
),
|
||||
Local
|
||||
.from_local_datetime(
|
||||
&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(),
|
||||
);
|
||||
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
@ -619,6 +653,26 @@ mod tests {
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takes_datetime() {
|
||||
let timezone_option = Some(Spanned {
|
||||
item: Zone::Local,
|
||||
span: Span::test_data(),
|
||||
});
|
||||
let args = Arguments {
|
||||
zone_options: timezone_option,
|
||||
format_options: None,
|
||||
cell_paths: None,
|
||||
};
|
||||
let expected = Value::date(
|
||||
Local.timestamp_opt(1614434140, 0).unwrap().into(),
|
||||
Span::test_data(),
|
||||
);
|
||||
let actual = action(&expected, &args, Span::test_data());
|
||||
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takes_timestamp_without_timezone() {
|
||||
let date_str = Value::test_string("1614434140000000000");
|
||||
|
@ -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();
|
||||
match input {
|
||||
Value::Filesize { .. } => input.clone(),
|
||||
|
@ -22,6 +22,7 @@ impl Command for SubCommand {
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("into glob")
|
||||
.input_output_types(vec![
|
||||
(Type::Glob, Type::Glob),
|
||||
(Type::String, Type::Glob),
|
||||
(
|
||||
Type::List(Box::new(Type::String)),
|
||||
@ -64,6 +65,11 @@ impl Command for SubCommand {
|
||||
example: "'1234' | into glob",
|
||||
result: Some(Value::test_glob("1234")),
|
||||
},
|
||||
Example {
|
||||
description: "convert glob to glob",
|
||||
example: "'1234' | into glob | into glob",
|
||||
result: Some(Value::test_glob("1234")),
|
||||
},
|
||||
Example {
|
||||
description: "convert filepath to glob",
|
||||
example: "ls Cargo.toml | get name | into glob",
|
||||
@ -94,6 +100,7 @@ fn glob_helper(
|
||||
fn action(input: &Value, _args: &Arguments, span: Span) -> Value {
|
||||
match input {
|
||||
Value::String { val, .. } => Value::glob(val.to_string(), false, span),
|
||||
Value::Glob { .. } => input.clone(),
|
||||
x => Value::error(
|
||||
ShellError::CantConvert {
|
||||
to_type: String::from("glob"),
|
||||
|
@ -253,13 +253,13 @@ fn action(input: &Value, args: &Arguments, span: Span) -> Value {
|
||||
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(
|
||||
{
|
||||
if radix == 10 {
|
||||
*val as i64
|
||||
} 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,
|
||||
_ => {
|
||||
return Value::error(
|
||||
|
@ -99,6 +99,11 @@ impl Command for SubCommand {
|
||||
"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,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{into_code, Config};
|
||||
use nu_protocol::{shell_error::into_code, Config};
|
||||
use nu_utils::get_system_locale;
|
||||
use num_format::ToFormattedString;
|
||||
|
||||
@ -38,6 +38,7 @@ impl Command for SubCommand {
|
||||
(Type::Filesize, Type::String),
|
||||
(Type::Date, Type::String),
|
||||
(Type::Duration, Type::String),
|
||||
(Type::Range, Type::String),
|
||||
(
|
||||
Type::List(Box::new(Type::Any)),
|
||||
Type::List(Box::new(Type::String)),
|
||||
@ -127,8 +128,8 @@ impl Command for SubCommand {
|
||||
},
|
||||
Example {
|
||||
description: "convert filesize to string",
|
||||
example: "1KiB | into string",
|
||||
result: Some(Value::test_string("1.0 KiB")),
|
||||
example: "1kB | into string",
|
||||
result: Some(Value::test_string("1.0 kB")),
|
||||
},
|
||||
Example {
|
||||
description: "convert duration to string",
|
||||
|
@ -1,9 +1,9 @@
|
||||
use crate::parse_date_from_string;
|
||||
use fancy_regex::{Regex, RegexBuilder};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::PipelineIterator;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IntoValue;
|
||||
@ -18,7 +18,7 @@ impl Command for IntoValue {
|
||||
.input_output_types(vec![(Type::table(), Type::table())])
|
||||
.named(
|
||||
"columns",
|
||||
SyntaxShape::Table(vec![]),
|
||||
SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
||||
"list of columns to update",
|
||||
Some('c'),
|
||||
)
|
||||
@ -143,7 +143,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
let val_str = val.coerce_str().unwrap_or_default();
|
||||
|
||||
// step 2: bounce string up against regexes
|
||||
if BOOLEAN_RE.is_match(&val_str) {
|
||||
if BOOLEAN_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let bval = val_str
|
||||
.parse::<bool>()
|
||||
.map_err(|_| ShellError::CantConvert {
|
||||
@ -156,12 +156,12 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
})?;
|
||||
|
||||
Ok(Value::bool(bval, span))
|
||||
} else if FLOAT_RE.is_match(&val_str) {
|
||||
} else if FLOAT_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let fval = val_str
|
||||
.parse::<f64>()
|
||||
.map_err(|_| ShellError::CantConvert {
|
||||
to_type: "string".to_string(),
|
||||
from_type: "float".to_string(),
|
||||
to_type: "float".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
r#""{val_str}" does not represent a valid floating point value"#
|
||||
@ -169,12 +169,12 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
})?;
|
||||
|
||||
Ok(Value::float(fval, span))
|
||||
} else if INTEGER_RE.is_match(&val_str) {
|
||||
} else if INTEGER_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let ival = val_str
|
||||
.parse::<i64>()
|
||||
.map_err(|_| ShellError::CantConvert {
|
||||
to_type: "string".to_string(),
|
||||
from_type: "int".to_string(),
|
||||
to_type: "int".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
r#""{val_str}" does not represent a valid integer value"#
|
||||
@ -186,15 +186,15 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
} else {
|
||||
Ok(Value::int(ival, span))
|
||||
}
|
||||
} else if INTEGER_WITH_DELIMS_RE.is_match(&val_str) {
|
||||
} else if INTEGER_WITH_DELIMS_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let mut val_str = val_str.into_owned();
|
||||
val_str.retain(|x| !['_', ','].contains(&x));
|
||||
|
||||
let ival = val_str
|
||||
.parse::<i64>()
|
||||
.map_err(|_| ShellError::CantConvert {
|
||||
to_type: "string".to_string(),
|
||||
from_type: "int".to_string(),
|
||||
to_type: "int".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
r#""{val_str}" does not represent a valid integer value"#
|
||||
@ -206,7 +206,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
} else {
|
||||
Ok(Value::int(ival, span))
|
||||
}
|
||||
} else if DATETIME_DMY_RE.is_match(&val_str) {
|
||||
} else if DATETIME_DMY_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||
to_type: "date".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
@ -217,7 +217,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
})?;
|
||||
|
||||
Ok(Value::date(dt, span))
|
||||
} else if DATETIME_YMD_RE.is_match(&val_str) {
|
||||
} else if DATETIME_YMD_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||
to_type: "date".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
@ -228,7 +228,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
||||
})?;
|
||||
|
||||
Ok(Value::date(dt, span))
|
||||
} else if DATETIME_YMDZ_RE.is_match(&val_str) {
|
||||
} else if DATETIME_YMDZ_RE.is_match(&val_str).unwrap_or(false) {
|
||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||
to_type: "date".to_string(),
|
||||
from_type: "string".to_string(),
|
||||
@ -271,8 +271,9 @@ const DATETIME_DMY_PATTERN: &str = r#"(?x)
|
||||
$
|
||||
"#;
|
||||
|
||||
static DATETIME_DMY_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(DATETIME_DMY_PATTERN).expect("datetime_dmy_pattern should be valid"));
|
||||
static DATETIME_DMY_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(DATETIME_DMY_PATTERN).expect("datetime_dmy_pattern should be valid")
|
||||
});
|
||||
const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
||||
^
|
||||
['"]? # optional quotes
|
||||
@ -297,8 +298,9 @@ const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
||||
['"]? # optional quotes
|
||||
$
|
||||
"#;
|
||||
static DATETIME_YMD_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(DATETIME_YMD_PATTERN).expect("datetime_ymd_pattern should be valid"));
|
||||
static DATETIME_YMD_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(DATETIME_YMD_PATTERN).expect("datetime_ymd_pattern should be valid")
|
||||
});
|
||||
//2023-03-24 16:44:17.865147299 -05:00
|
||||
const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
||||
^
|
||||
@ -331,23 +333,24 @@ const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
||||
['"]? # optional quotes
|
||||
$
|
||||
"#;
|
||||
static DATETIME_YMDZ_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(DATETIME_YMDZ_PATTERN).expect("datetime_ymdz_pattern should be valid"));
|
||||
static DATETIME_YMDZ_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
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+\.)$")
|
||||
.expect("float pattern should be valid")
|
||||
});
|
||||
|
||||
static INTEGER_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^\s*-?(\d+)$").expect("integer pattern should be valid"));
|
||||
static INTEGER_RE: LazyLock<Regex> =
|
||||
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})+)$")
|
||||
.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)$")
|
||||
.case_insensitive(true)
|
||||
.build()
|
||||
@ -369,118 +372,154 @@ mod test {
|
||||
#[test]
|
||||
fn test_float_parse() {
|
||||
// The regex should work on all these but nushell's float parser is more strict
|
||||
assert!(FLOAT_RE.is_match("0.1"));
|
||||
assert!(FLOAT_RE.is_match("3.0"));
|
||||
assert!(FLOAT_RE.is_match("3.00001"));
|
||||
assert!(FLOAT_RE.is_match("-9.9990e-003"));
|
||||
assert!(FLOAT_RE.is_match("9.9990e+003"));
|
||||
assert!(FLOAT_RE.is_match("9.9990E+003"));
|
||||
assert!(FLOAT_RE.is_match("9.9990E+003"));
|
||||
assert!(FLOAT_RE.is_match(".5"));
|
||||
assert!(FLOAT_RE.is_match("2.5E-10"));
|
||||
assert!(FLOAT_RE.is_match("2.5e10"));
|
||||
assert!(FLOAT_RE.is_match("NaN"));
|
||||
assert!(FLOAT_RE.is_match("-NaN"));
|
||||
assert!(FLOAT_RE.is_match("-inf"));
|
||||
assert!(FLOAT_RE.is_match("inf"));
|
||||
assert!(FLOAT_RE.is_match("-7e-05"));
|
||||
assert!(FLOAT_RE.is_match("7e-05"));
|
||||
assert!(FLOAT_RE.is_match("+7e+05"));
|
||||
assert!(FLOAT_RE.is_match("0.1").unwrap());
|
||||
assert!(FLOAT_RE.is_match("3.0").unwrap());
|
||||
assert!(FLOAT_RE.is_match("3.00001").unwrap());
|
||||
assert!(FLOAT_RE.is_match("-9.9990e-003").unwrap());
|
||||
assert!(FLOAT_RE.is_match("9.9990e+003").unwrap());
|
||||
assert!(FLOAT_RE.is_match("9.9990E+003").unwrap());
|
||||
assert!(FLOAT_RE.is_match("9.9990E+003").unwrap());
|
||||
assert!(FLOAT_RE.is_match(".5").unwrap());
|
||||
assert!(FLOAT_RE.is_match("2.5E-10").unwrap());
|
||||
assert!(FLOAT_RE.is_match("2.5e10").unwrap());
|
||||
assert!(FLOAT_RE.is_match("NaN").unwrap());
|
||||
assert!(FLOAT_RE.is_match("-NaN").unwrap());
|
||||
assert!(FLOAT_RE.is_match("-inf").unwrap());
|
||||
assert!(FLOAT_RE.is_match("inf").unwrap());
|
||||
assert!(FLOAT_RE.is_match("-7e-05").unwrap());
|
||||
assert!(FLOAT_RE.is_match("7e-05").unwrap());
|
||||
assert!(FLOAT_RE.is_match("+7e+05").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_parse() {
|
||||
assert!(INTEGER_RE.is_match("0"));
|
||||
assert!(INTEGER_RE.is_match("1"));
|
||||
assert!(INTEGER_RE.is_match("10"));
|
||||
assert!(INTEGER_RE.is_match("100"));
|
||||
assert!(INTEGER_RE.is_match("1000"));
|
||||
assert!(INTEGER_RE.is_match("10000"));
|
||||
assert!(INTEGER_RE.is_match("100000"));
|
||||
assert!(INTEGER_RE.is_match("1000000"));
|
||||
assert!(INTEGER_RE.is_match("10000000"));
|
||||
assert!(INTEGER_RE.is_match("100000000"));
|
||||
assert!(INTEGER_RE.is_match("1000000000"));
|
||||
assert!(INTEGER_RE.is_match("10000000000"));
|
||||
assert!(INTEGER_RE.is_match("100000000000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000_000_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000_000_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000_000_000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100,000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000,000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000,000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100,000,000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000,000,000"));
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000,000,000"));
|
||||
assert!(INTEGER_RE.is_match("0").unwrap());
|
||||
assert!(INTEGER_RE.is_match("1").unwrap());
|
||||
assert!(INTEGER_RE.is_match("10").unwrap());
|
||||
assert!(INTEGER_RE.is_match("100").unwrap());
|
||||
assert!(INTEGER_RE.is_match("1000").unwrap());
|
||||
assert!(INTEGER_RE.is_match("10000").unwrap());
|
||||
assert!(INTEGER_RE.is_match("100000").unwrap());
|
||||
assert!(INTEGER_RE.is_match("1000000").unwrap());
|
||||
assert!(INTEGER_RE.is_match("10000000").unwrap());
|
||||
assert!(INTEGER_RE.is_match("100000000").unwrap());
|
||||
assert!(INTEGER_RE.is_match("1000000000").unwrap());
|
||||
assert!(INTEGER_RE.is_match("10000000000").unwrap());
|
||||
assert!(INTEGER_RE.is_match("100000000000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000_000_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000_000_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000_000_000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100,000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000,000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000,000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("100,000,000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000,000,000").unwrap());
|
||||
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000,000,000").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bool_parse() {
|
||||
assert!(BOOLEAN_RE.is_match("true"));
|
||||
assert!(BOOLEAN_RE.is_match("false"));
|
||||
assert!(!BOOLEAN_RE.is_match("1"));
|
||||
assert!(!BOOLEAN_RE.is_match("0"));
|
||||
assert!(BOOLEAN_RE.is_match("true").unwrap());
|
||||
assert!(BOOLEAN_RE.is_match("false").unwrap());
|
||||
assert!(!BOOLEAN_RE.is_match("1").unwrap());
|
||||
assert!(!BOOLEAN_RE.is_match("0").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_ymdz_pattern() {
|
||||
assert!(DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00Z"));
|
||||
assert!(DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789Z"));
|
||||
assert!(DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00+01:00"));
|
||||
assert!(DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789+01:00"));
|
||||
assert!(DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00-01:00"));
|
||||
assert!(DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789-01:00"));
|
||||
assert!(DATETIME_YMDZ_RE.is_match("'2022-01-01T00:00:00Z'"));
|
||||
assert!(DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00Z").unwrap());
|
||||
assert!(DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789Z")
|
||||
.unwrap());
|
||||
assert!(DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00+01:00")
|
||||
.unwrap());
|
||||
assert!(DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789+01:00")
|
||||
.unwrap());
|
||||
assert!(DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00-01:00")
|
||||
.unwrap());
|
||||
assert!(DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789-01:00")
|
||||
.unwrap());
|
||||
assert!(DATETIME_YMDZ_RE.is_match("'2022-01-01T00:00:00Z'").unwrap());
|
||||
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00."));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00+01"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00+01:0"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00+1:00"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789+01"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789+01:0"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789+1:00"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00-01"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00-01:0"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00-1:00"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789-01"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789-01:0"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.123456789-1:00"));
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00").unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.").unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00+01").unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00+01:0")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00+1:00")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789+01")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789+01:0")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789+1:00")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00-01").unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00-01:0")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00-1:00")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789-01")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789-01:0")
|
||||
.unwrap());
|
||||
assert!(!DATETIME_YMDZ_RE
|
||||
.is_match("2022-01-01T00:00:00.123456789-1:00")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_ymd_pattern() {
|
||||
assert!(DATETIME_YMD_RE.is_match("2022-01-01"));
|
||||
assert!(DATETIME_YMD_RE.is_match("2022/01/01"));
|
||||
assert!(DATETIME_YMD_RE.is_match("2022-01-01T00:00:00"));
|
||||
assert!(DATETIME_YMD_RE.is_match("2022-01-01T00:00:00.000000000"));
|
||||
assert!(DATETIME_YMD_RE.is_match("'2022-01-01'"));
|
||||
assert!(DATETIME_YMD_RE.is_match("2022-01-01").unwrap());
|
||||
assert!(DATETIME_YMD_RE.is_match("2022/01/01").unwrap());
|
||||
assert!(DATETIME_YMD_RE.is_match("2022-01-01T00:00:00").unwrap());
|
||||
assert!(DATETIME_YMD_RE
|
||||
.is_match("2022-01-01T00:00:00.000000000")
|
||||
.unwrap());
|
||||
assert!(DATETIME_YMD_RE.is_match("'2022-01-01'").unwrap());
|
||||
|
||||
// The regex isn't this specific, but it would be nice if it were
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-13-01"));
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-01-32"));
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T24:00:00"));
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T00:60:00"));
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T00:00:60"));
|
||||
assert!(!DATETIME_YMD_RE.is_match("2022-01-01T00:00:00.0000000000"));
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-13-01").unwrap());
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-01-32").unwrap());
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T24:00:00").unwrap());
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T00:60:00").unwrap());
|
||||
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T00:00:60").unwrap());
|
||||
assert!(!DATETIME_YMD_RE
|
||||
.is_match("2022-01-01T00:00:00.0000000000")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_dmy_pattern() {
|
||||
assert!(DATETIME_DMY_RE.is_match("31-12-2021"));
|
||||
assert!(DATETIME_DMY_RE.is_match("01/01/2022"));
|
||||
assert!(DATETIME_DMY_RE.is_match("15-06-2023 12:30"));
|
||||
assert!(!DATETIME_DMY_RE.is_match("2022-13-01"));
|
||||
assert!(!DATETIME_DMY_RE.is_match("2022-01-32"));
|
||||
assert!(!DATETIME_DMY_RE.is_match("2022-01-01 24:00"));
|
||||
assert!(DATETIME_DMY_RE.is_match("31-12-2021").unwrap());
|
||||
assert!(DATETIME_DMY_RE.is_match("01/01/2022").unwrap());
|
||||
assert!(DATETIME_DMY_RE.is_match("15-06-2023 12:30").unwrap());
|
||||
assert!(!DATETIME_DMY_RE.is_match("2022-13-01").unwrap());
|
||||
assert!(!DATETIME_DMY_RE.is_match("2022-01-32").unwrap());
|
||||
assert!(!DATETIME_DMY_RE.is_match("2022-01-01 24:00").unwrap());
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user