mirror of
https://github.com/nushell/nushell.git
synced 2025-07-04 16:40:47 +02:00
Compare commits
217 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
e911ff4d67 | |||
28b6db115a | |||
e735bd475f | |||
299d199150 | |||
5e784d38eb | |||
868029f655 | |||
043d1ed9fb | |||
6230a62e9e | |||
71b49c3374 | |||
2eef42c6b9 | |||
0209992f6c | |||
c9d54f821b | |||
59d6dee3b3 | |||
9d25b2f29a | |||
91ff57faa7 | |||
b99affba4b | |||
639bd4fc2e | |||
a0f38f8845 | |||
a11c9e9d70 | |||
bdbcf82967 | |||
1f47d72e86 | |||
d83781ddec | |||
e32e55938b | |||
de08b68ba8 | |||
0e3a8c552c | |||
389e7d2502 | |||
fce6146576 | |||
02313e6819 | |||
df0a174802 | |||
bcb7ef48b6 | |||
9f714e62cb | |||
a95c2198a6 | |||
2df91e7f92 | |||
44be445b57 | |||
e43632fd95 | |||
69e4abad0f | |||
3bedbd0669 | |||
1d15bbc95b | |||
5002d87af4 | |||
2a3805c164 | |||
52f646d8db | |||
36c1073441 | |||
2979595cc5 | |||
d67120be19 | |||
ad31f1cf26 | |||
99798ace7d | |||
ba4becc61c | |||
397499b106 | |||
55c3fc9141 | |||
2830ec008c | |||
4c8b09eb97 | |||
98e0864be8 | |||
6dc71f5ad0 | |||
d6f4e4c4fe | |||
33ae71f300 | |||
abcca0897e | |||
b1379b2b14 | |||
27ebccce80 | |||
6964968f14 | |||
68377c176d | |||
baadaee016 | |||
199aa2ad3a | |||
29b176b719 | |||
6ce20675eb | |||
1e9967c3bf | |||
e0bc85d0dd | |||
e3fd4d3f81 | |||
00709fc5bd | |||
157494e803 | |||
f03ba6793e | |||
475aa4f1dd | |||
1d6ac16530 | |||
573a7e2c7b | |||
cebbc82322 | |||
cf5b2aeb88 | |||
52eb9c2ef3 | |||
702dcd8581 | |||
9e6ada6411 | |||
a38663ec90 | |||
02804ab537 | |||
b2d0d9cf13 | |||
46589faaca | |||
166d5fa4ff | |||
4bd38847c2 | |||
30a4187be4 | |||
f0c83a4459 | |||
fc61416c79 | |||
8200831b07 | |||
497954d84c | |||
bcaef8959c | |||
5bef81a059 | |||
d68c3ec89a | |||
0c72f881a6 | |||
8195e2d638 | |||
e8c20390e0 | |||
13df0af514 | |||
54e9aa92bc | |||
1afff777a6 | |||
071faae772 | |||
08a241f763 | |||
63f9e273b3 | |||
71d604067a | |||
66d0e18674 | |||
a940a8aa80 | |||
0d30550950 | |||
65bb0ff167 | |||
151767a5e3 | |||
a948ec6c2c | |||
28a7461057 | |||
6f47990a63 | |||
183c2221bb | |||
03ee54a4df | |||
2541a712e4 | |||
ee877607fb | |||
93351b889a | |||
5fa9d76500 | |||
cd0d0364ec | |||
cf5fec63c0 | |||
5c5cf418fb | |||
fb14008f50 | |||
18c8c16c5e | |||
299a218de7 | |||
1a081c09de |
2
.github/workflows/audit.yml
vendored
2
.github/workflows/audit.yml
vendored
@ -20,6 +20,6 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.7
|
||||||
- uses: rustsec/audit-check@v1.4.1
|
- uses: rustsec/audit-check@v2.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Setup Rust toolchain and cache
|
- name: Setup Rust toolchain and cache
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||||
|
|
||||||
- name: cargo fmt
|
- name: cargo fmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
@ -64,7 +64,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Setup Rust toolchain and cache
|
- name: Setup Rust toolchain and cache
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: cargo test --workspace --profile ci --exclude nu_plugin_*
|
run: cargo test --workspace --profile ci --exclude nu_plugin_*
|
||||||
@ -93,7 +93,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Setup Rust toolchain and cache
|
- name: Setup Rust toolchain and cache
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||||
|
|
||||||
- name: Install Nushell
|
- name: Install Nushell
|
||||||
run: cargo install --path . --locked --no-default-features
|
run: cargo install --path . --locked --no-default-features
|
||||||
@ -144,7 +144,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Setup Rust toolchain and cache
|
- name: Setup Rust toolchain and cache
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||||
|
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS
|
run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS
|
||||||
|
30
.github/workflows/milestone.yml
vendored
Normal file
30
.github/workflows/milestone.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Description:
|
||||||
|
# - 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]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-milestone:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Milestone Update
|
||||||
|
steps:
|
||||||
|
- 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@main
|
||||||
|
if: github.event.issue.state == 'closed'
|
||||||
|
with:
|
||||||
|
action: bind-issue
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
27
.github/workflows/nightly-build.yml
vendored
27
.github/workflows/nightly-build.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
|||||||
# if: github.repository == 'nushell/nightly'
|
# if: github.repository == 'nushell/nightly'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4
|
||||||
if: github.repository == 'nushell/nightly'
|
if: github.repository == 'nushell/nightly'
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
@ -36,10 +36,10 @@ jobs:
|
|||||||
token: ${{ secrets.WORKFLOW_TOKEN }}
|
token: ${{ secrets.WORKFLOW_TOKEN }}
|
||||||
|
|
||||||
- name: Setup Nushell
|
- name: Setup Nushell
|
||||||
uses: hustcer/setup-nu@v3.13
|
uses: hustcer/setup-nu@v3
|
||||||
if: github.repository == 'nushell/nightly'
|
if: github.repository == 'nushell/nightly'
|
||||||
with:
|
with:
|
||||||
version: 0.97.1
|
version: 0.98.0
|
||||||
|
|
||||||
# Synchronize the main branch of nightly repo with the main branch of Nushell official repo
|
# Synchronize the main branch of nightly repo with the main branch of Nushell official repo
|
||||||
- name: Prepare for Nightly Release
|
- name: Prepare for Nightly Release
|
||||||
@ -65,7 +65,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
standard:
|
standard:
|
||||||
name: Std
|
name: Nu
|
||||||
needs: prepare
|
needs: prepare
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -82,6 +82,7 @@ jobs:
|
|||||||
- armv7-unknown-linux-gnueabihf
|
- armv7-unknown-linux-gnueabihf
|
||||||
- armv7-unknown-linux-musleabihf
|
- armv7-unknown-linux-musleabihf
|
||||||
- riscv64gc-unknown-linux-gnu
|
- riscv64gc-unknown-linux-gnu
|
||||||
|
- loongarch64-unknown-linux-gnu
|
||||||
extra: ['bin']
|
extra: ['bin']
|
||||||
include:
|
include:
|
||||||
- target: aarch64-apple-darwin
|
- target: aarch64-apple-darwin
|
||||||
@ -114,11 +115,13 @@ jobs:
|
|||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
- target: riscv64gc-unknown-linux-gnu
|
- target: riscv64gc-unknown-linux-gnu
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
- target: loongarch64-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
|
||||||
runs-on: ${{matrix.os}}
|
runs-on: ${{matrix.os}}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@ -128,15 +131,15 @@ jobs:
|
|||||||
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
|
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
|
||||||
|
|
||||||
- name: Setup Rust toolchain and cache
|
- name: Setup Rust toolchain and cache
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||||
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
|
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
|
||||||
with:
|
with:
|
||||||
rustflags: ''
|
rustflags: ''
|
||||||
|
|
||||||
- name: Setup Nushell
|
- name: Setup Nushell
|
||||||
uses: hustcer/setup-nu@v3.13
|
uses: hustcer/setup-nu@v3
|
||||||
with:
|
with:
|
||||||
version: 0.97.1
|
version: 0.98.0
|
||||||
|
|
||||||
- name: Release Nu Binary
|
- name: Release Nu Binary
|
||||||
id: nu
|
id: nu
|
||||||
@ -167,7 +170,7 @@ jobs:
|
|||||||
# REF: https://github.com/marketplace/actions/gh-release
|
# REF: https://github.com/marketplace/actions/gh-release
|
||||||
# Create a release only in nushell/nightly repo
|
# Create a release only in nushell/nightly repo
|
||||||
- name: Publish Archive
|
- name: Publish Archive
|
||||||
uses: softprops/action-gh-release@v2.0.8
|
uses: softprops/action-gh-release@v2.0.9
|
||||||
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
|
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
@ -187,14 +190,14 @@ jobs:
|
|||||||
- name: Waiting for Release
|
- name: Waiting for Release
|
||||||
run: sleep 1800
|
run: sleep 1800
|
||||||
|
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Setup Nushell
|
- name: Setup Nushell
|
||||||
uses: hustcer/setup-nu@v3.13
|
uses: hustcer/setup-nu@v3
|
||||||
with:
|
with:
|
||||||
version: 0.97.1
|
version: 0.98.0
|
||||||
|
|
||||||
# Keep the last a few releases
|
# Keep the last a few releases
|
||||||
- name: Delete Older Releases
|
- name: Delete Older Releases
|
||||||
|
7
.github/workflows/release-pkg.nu
vendored
7
.github/workflows/release-pkg.nu
vendored
@ -98,6 +98,13 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
|
|||||||
$env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER = 'armv7r-linux-musleabihf-gcc'
|
$env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER = 'armv7r-linux-musleabihf-gcc'
|
||||||
cargo-build-nu
|
cargo-build-nu
|
||||||
}
|
}
|
||||||
|
'loongarch64-unknown-linux-gnu' => {
|
||||||
|
aria2c https://github.com/loongson/build-tools/releases/download/2024.08.08/x86_64-cross-tools-loongarch64-binutils_2.43-gcc_14.2.0-glibc_2.40.tar.xz
|
||||||
|
tar xf x86_64-cross-tools-loongarch64-*.tar.xz
|
||||||
|
$env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.PWD)/cross-tools/bin')
|
||||||
|
$env.CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_GNU_LINKER = 'loongarch64-unknown-linux-gnu-gcc'
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
# musl-tools to fix 'Failed to find tool. Is `musl-gcc` installed?'
|
# musl-tools to fix 'Failed to find tool. Is `musl-gcc` installed?'
|
||||||
# Actually just for x86_64-unknown-linux-musl target
|
# Actually just for x86_64-unknown-linux-musl target
|
||||||
|
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
@ -14,8 +14,8 @@ defaults:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
standard:
|
release:
|
||||||
name: Std
|
name: Nu
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -32,6 +32,7 @@ jobs:
|
|||||||
- armv7-unknown-linux-gnueabihf
|
- armv7-unknown-linux-gnueabihf
|
||||||
- armv7-unknown-linux-musleabihf
|
- armv7-unknown-linux-musleabihf
|
||||||
- riscv64gc-unknown-linux-gnu
|
- riscv64gc-unknown-linux-gnu
|
||||||
|
- loongarch64-unknown-linux-gnu
|
||||||
extra: ['bin']
|
extra: ['bin']
|
||||||
include:
|
include:
|
||||||
- target: aarch64-apple-darwin
|
- target: aarch64-apple-darwin
|
||||||
@ -64,27 +65,29 @@ jobs:
|
|||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
- target: riscv64gc-unknown-linux-gnu
|
- target: riscv64gc-unknown-linux-gnu
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
- target: loongarch64-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
|
||||||
runs-on: ${{matrix.os}}
|
runs-on: ${{matrix.os}}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Update Rust Toolchain Target
|
- name: Update Rust Toolchain Target
|
||||||
run: |
|
run: |
|
||||||
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
|
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
|
||||||
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||||
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
|
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
|
||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
rustflags: ''
|
rustflags: ''
|
||||||
|
|
||||||
- name: Setup Nushell
|
- name: Setup Nushell
|
||||||
uses: hustcer/setup-nu@v3.13
|
uses: hustcer/setup-nu@v3
|
||||||
with:
|
with:
|
||||||
version: 0.97.1
|
version: 0.98.0
|
||||||
|
|
||||||
- name: Release Nu Binary
|
- name: Release Nu Binary
|
||||||
id: nu
|
id: nu
|
||||||
@ -95,12 +98,36 @@ jobs:
|
|||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
_EXTRA_: ${{ matrix.extra }}
|
_EXTRA_: ${{ matrix.extra }}
|
||||||
|
|
||||||
# REF: https://github.com/marketplace/actions/gh-release
|
# WARN: Don't upgrade this action due to the release per asset issue.
|
||||||
|
# See: https://github.com/softprops/action-gh-release/issues/445
|
||||||
- name: Publish Archive
|
- name: Publish Archive
|
||||||
uses: softprops/action-gh-release@v2.0.8
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
files: ${{ steps.nu.outputs.archive }}
|
files: ${{ steps.nu.outputs.archive }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
sha256sum:
|
||||||
|
needs: release
|
||||||
|
name: Create Sha256sum
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download Release Archives
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: >-
|
||||||
|
gh release download ${{ github.ref_name }}
|
||||||
|
--repo ${{ github.repository }}
|
||||||
|
--pattern '*'
|
||||||
|
--dir release
|
||||||
|
- name: Create Checksums
|
||||||
|
run: cd release && shasum -a 256 * > ../SHA256SUMS
|
||||||
|
- name: Publish Checksums
|
||||||
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
files: SHA256SUMS
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@ -10,4 +10,4 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Check spelling
|
- name: Check spelling
|
||||||
uses: crate-ci/typos@v1.24.5
|
uses: crate-ci/typos@v1.27.0
|
||||||
|
649
Cargo.lock
generated
649
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
74
Cargo.toml
74
Cargo.toml
@ -10,8 +10,8 @@ homepage = "https://www.nushell.sh"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu"
|
name = "nu"
|
||||||
repository = "https://github.com/nushell/nushell"
|
repository = "https://github.com/nushell/nushell"
|
||||||
rust-version = "1.79.0"
|
rust-version = "1.80.1"
|
||||||
version = "0.98.0"
|
version = "0.100.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -66,8 +66,8 @@ alphanumeric-sort = "1.5"
|
|||||||
ansi-str = "0.8"
|
ansi-str = "0.8"
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bracoxide = "0.1.2"
|
bracoxide = "0.1.4"
|
||||||
brotli = "5.0"
|
brotli = "6.0"
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytesize = "1.3"
|
bytesize = "1.3"
|
||||||
@ -75,7 +75,7 @@ calamine = "0.24.0"
|
|||||||
chardetng = "0.1.17"
|
chardetng = "0.1.17"
|
||||||
chrono = { default-features = false, version = "0.4.34" }
|
chrono = { default-features = false, version = "0.4.34" }
|
||||||
chrono-humanize = "0.2.3"
|
chrono-humanize = "0.2.3"
|
||||||
chrono-tz = "0.8"
|
chrono-tz = "0.10"
|
||||||
crossbeam-channel = "0.5.8"
|
crossbeam-channel = "0.5.8"
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
csv = "1.3"
|
csv = "1.3"
|
||||||
@ -86,13 +86,13 @@ dirs = "5.0"
|
|||||||
dirs-sys = "0.4"
|
dirs-sys = "0.4"
|
||||||
dtparse = "2.0"
|
dtparse = "2.0"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
fancy-regex = "0.13"
|
fancy-regex = "0.14"
|
||||||
filesize = "0.2"
|
filesize = "0.2"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
fuzzy-matcher = "0.3"
|
fuzzy-matcher = "0.3"
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
human-date-parser = "0.1.1"
|
human-date-parser = "0.2.0"
|
||||||
indexmap = "2.5"
|
indexmap = "2.6"
|
||||||
indicatif = "0.17"
|
indicatif = "0.17"
|
||||||
interprocess = "2.2.0"
|
interprocess = "2.2.0"
|
||||||
is_executable = "1.0"
|
is_executable = "1.0"
|
||||||
@ -103,7 +103,7 @@ log = "0.4"
|
|||||||
lru = "0.12"
|
lru = "0.12"
|
||||||
lscolors = { version = "0.17", default-features = false }
|
lscolors = { version = "0.17", default-features = false }
|
||||||
lsp-server = "0.7.5"
|
lsp-server = "0.7.5"
|
||||||
lsp-types = "0.95.0"
|
lsp-types = { version = "0.95.0", features = ["proposed"] }
|
||||||
mach2 = "0.4"
|
mach2 = "0.4"
|
||||||
md5 = { version = "0.10", package = "md-5" }
|
md5 = { version = "0.10", package = "md-5" }
|
||||||
miette = "7.2"
|
miette = "7.2"
|
||||||
@ -117,8 +117,8 @@ notify-debouncer-full = { version = "0.3", default-features = false }
|
|||||||
nu-ansi-term = "0.50.1"
|
nu-ansi-term = "0.50.1"
|
||||||
num-format = "0.4"
|
num-format = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
|
oem_cp = "2.0.0"
|
||||||
omnipath = "0.1"
|
omnipath = "0.1"
|
||||||
once_cell = "1.18"
|
|
||||||
open = "5.3"
|
open = "5.3"
|
||||||
os_pipe = { version = "1.2", features = ["io_safety"] }
|
os_pipe = { version = "1.2", features = ["io_safety"] }
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
@ -137,7 +137,7 @@ rand = "0.8"
|
|||||||
rand_chacha = "0.3.1"
|
rand_chacha = "0.3.1"
|
||||||
ratatui = "0.26"
|
ratatui = "0.26"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
reedline = "0.35.0"
|
reedline = "0.37.0"
|
||||||
regex = "1.9.5"
|
regex = "1.9.5"
|
||||||
rmp = "0.8"
|
rmp = "0.8"
|
||||||
rmp-serde = "1.3"
|
rmp-serde = "1.3"
|
||||||
@ -153,15 +153,15 @@ serde_yaml = "0.9"
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
strip-ansi-escapes = "0.2.0"
|
strip-ansi-escapes = "0.2.0"
|
||||||
syn = "2.0"
|
syn = "2.0"
|
||||||
sysinfo = "0.30"
|
sysinfo = "0.32"
|
||||||
tabled = { version = "0.16.0", default-features = false }
|
tabled = { version = "0.16.0", default-features = false }
|
||||||
tempfile = "3.10"
|
tempfile = "3.13"
|
||||||
terminal_size = "0.3"
|
terminal_size = "0.3"
|
||||||
titlecase = "2.0"
|
titlecase = "2.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
trash = "3.3"
|
trash = "5.2"
|
||||||
umask = "2.1"
|
umask = "2.1"
|
||||||
unicode-segmentation = "1.11"
|
unicode-segmentation = "1.12"
|
||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
ureq = { version = "2.10", default-features = false }
|
ureq = { version = "2.10", default-features = false }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
@ -172,11 +172,11 @@ uu_mv = "0.0.27"
|
|||||||
uu_whoami = "0.0.27"
|
uu_whoami = "0.0.27"
|
||||||
uu_uname = "0.0.27"
|
uu_uname = "0.0.27"
|
||||||
uucore = "0.0.27"
|
uucore = "0.0.27"
|
||||||
uuid = "1.10.0"
|
uuid = "1.11.0"
|
||||||
v_htmlescape = "0.15.0"
|
v_htmlescape = "0.15.0"
|
||||||
wax = "0.6"
|
wax = "0.6"
|
||||||
which = "6.0.0"
|
which = "6.0.0"
|
||||||
windows = "0.54"
|
windows = "0.56"
|
||||||
windows-sys = "0.48"
|
windows-sys = "0.48"
|
||||||
winreg = "0.52"
|
winreg = "0.52"
|
||||||
|
|
||||||
@ -189,22 +189,22 @@ unchecked_duration_subtraction = "warn"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cli = { path = "./crates/nu-cli", version = "0.98.0" }
|
nu-cli = { path = "./crates/nu-cli", version = "0.100.0" }
|
||||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.98.0" }
|
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.100.0" }
|
||||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.98.0" }
|
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.100.0" }
|
||||||
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.98.0", optional = true }
|
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.100.0", optional = true }
|
||||||
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.98.0" }
|
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.100.0" }
|
||||||
nu-command = { path = "./crates/nu-command", version = "0.98.0" }
|
nu-command = { path = "./crates/nu-command", version = "0.100.0" }
|
||||||
nu-engine = { path = "./crates/nu-engine", version = "0.98.0" }
|
nu-engine = { path = "./crates/nu-engine", version = "0.100.0" }
|
||||||
nu-explore = { path = "./crates/nu-explore", version = "0.98.0" }
|
nu-explore = { path = "./crates/nu-explore", version = "0.100.0" }
|
||||||
nu-lsp = { path = "./crates/nu-lsp/", version = "0.98.0" }
|
nu-lsp = { path = "./crates/nu-lsp/", version = "0.100.0" }
|
||||||
nu-parser = { path = "./crates/nu-parser", version = "0.98.0" }
|
nu-parser = { path = "./crates/nu-parser", version = "0.100.0" }
|
||||||
nu-path = { path = "./crates/nu-path", version = "0.98.0" }
|
nu-path = { path = "./crates/nu-path", version = "0.100.0" }
|
||||||
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.98.0" }
|
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.100.0" }
|
||||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.98.0" }
|
nu-protocol = { path = "./crates/nu-protocol", version = "0.100.0" }
|
||||||
nu-std = { path = "./crates/nu-std", version = "0.98.0" }
|
nu-std = { path = "./crates/nu-std", version = "0.100.0" }
|
||||||
nu-system = { path = "./crates/nu-system", version = "0.98.0" }
|
nu-system = { path = "./crates/nu-system", version = "0.100.0" }
|
||||||
nu-utils = { path = "./crates/nu-utils", version = "0.98.0" }
|
nu-utils = { path = "./crates/nu-utils", version = "0.100.0" }
|
||||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||||
|
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
@ -234,12 +234,12 @@ nix = { workspace = true, default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-test-support = { path = "./crates/nu-test-support", version = "0.98.0" }
|
nu-test-support = { path = "./crates/nu-test-support", version = "0.100.0" }
|
||||||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.98.0" }
|
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.100.0" }
|
||||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.98.0" }
|
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.100.0" }
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
tango-bench = "0.5"
|
tango-bench = "0.6"
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rstest = { workspace = true, default-features = false }
|
rstest = { workspace = true, default-features = false }
|
||||||
|
@ -229,7 +229,7 @@ Please submit an issue or PR to be added to this list.
|
|||||||
See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed!
|
See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed!
|
||||||
|
|
||||||
<a href="https://github.com/nushell/nushell/graphs/contributors">
|
<a href="https://github.com/nushell/nushell/graphs/contributors">
|
||||||
<img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=750" />
|
<img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=750&columns=20" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -5,27 +5,27 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cli"
|
name = "nu-cli"
|
||||||
version = "0.98.0"
|
version = "0.100.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.98.0" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.100.0" }
|
||||||
nu-command = { path = "../nu-command", version = "0.98.0" }
|
nu-command = { path = "../nu-command", version = "0.100.0" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.98.0" }
|
nu-test-support = { path = "../nu-test-support", version = "0.100.0" }
|
||||||
rstest = { workspace = true, default-features = false }
|
rstest = { workspace = true, default-features = false }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.98.0" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.100.0" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.98.0" }
|
nu-engine = { path = "../nu-engine", version = "0.100.0" }
|
||||||
nu-path = { path = "../nu-path", version = "0.98.0" }
|
nu-path = { path = "../nu-path", version = "0.100.0" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.98.0" }
|
nu-parser = { path = "../nu-parser", version = "0.100.0" }
|
||||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.98.0", optional = true }
|
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.100.0", optional = true }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.98.0" }
|
nu-protocol = { path = "../nu-protocol", version = "0.100.0" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.98.0" }
|
nu-utils = { path = "../nu-utils", version = "0.100.0" }
|
||||||
nu-color-config = { path = "../nu-color-config", version = "0.98.0" }
|
nu-color-config = { path = "../nu-color-config", version = "0.100.0" }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||||
|
|
||||||
@ -37,7 +37,6 @@ is_executable = { workspace = true }
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||||
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
|
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
|
||||||
once_cell = { workspace = true }
|
|
||||||
percent-encoding = { workspace = true }
|
percent-encoding = { workspace = true }
|
||||||
sysinfo = { workspace = true }
|
sysinfo = { workspace = true }
|
||||||
unicode-segmentation = { workspace = true }
|
unicode-segmentation = { workspace = true }
|
||||||
@ -49,4 +48,4 @@ plugin = ["nu-plugin-engine"]
|
|||||||
system-clipboard = ["reedline/system_clipboard"]
|
system-clipboard = ["reedline/system_clipboard"]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
@ -17,6 +17,7 @@ pub fn add_cli_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
CommandlineGetCursor,
|
CommandlineGetCursor,
|
||||||
CommandlineSetCursor,
|
CommandlineSetCursor,
|
||||||
History,
|
History,
|
||||||
|
HistoryImport,
|
||||||
HistorySession,
|
HistorySession,
|
||||||
Keybindings,
|
Keybindings,
|
||||||
KeybindingsDefault,
|
KeybindingsDefault,
|
||||||
|
9
crates/nu-cli/src/commands/history/fields.rs
Normal file
9
crates/nu-cli/src/commands/history/fields.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Each const is named after a HistoryItem field, and the value is the field name to be displayed to
|
||||||
|
// the user (or accept during import).
|
||||||
|
pub const COMMAND_LINE: &str = "command";
|
||||||
|
pub const START_TIMESTAMP: &str = "start_timestamp";
|
||||||
|
pub const HOSTNAME: &str = "hostname";
|
||||||
|
pub const CWD: &str = "cwd";
|
||||||
|
pub const EXIT_STATUS: &str = "exit_status";
|
||||||
|
pub const DURATION: &str = "duration";
|
||||||
|
pub const SESSION_ID: &str = "session_id";
|
@ -5,6 +5,8 @@ use reedline::{
|
|||||||
SqliteBackedHistory,
|
SqliteBackedHistory,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::fields;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct History;
|
pub struct History;
|
||||||
|
|
||||||
@ -42,91 +44,76 @@ impl Command for History {
|
|||||||
let Some(history) = engine_state.history_config() else {
|
let Some(history) = engine_state.history_config() else {
|
||||||
return Ok(PipelineData::empty());
|
return Ok(PipelineData::empty());
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history`
|
// todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history`
|
||||||
if let Some(config_path) = nu_path::config_dir() {
|
let Some(history_path) = history.file_path() else {
|
||||||
let clear = call.has_flag(engine_state, stack, "clear")?;
|
return Err(ShellError::ConfigDirNotFound { span: Some(head) });
|
||||||
let long = call.has_flag(engine_state, stack, "long")?;
|
};
|
||||||
let signals = engine_state.signals().clone();
|
|
||||||
|
|
||||||
let mut history_path = config_path;
|
if call.has_flag(engine_state, stack, "clear")? {
|
||||||
history_path.push("nushell");
|
let _ = std::fs::remove_file(history_path);
|
||||||
match history.file_format {
|
// TODO: FIXME also clear the auxiliary files when using sqlite
|
||||||
HistoryFileFormat::Sqlite => {
|
return Ok(PipelineData::empty());
|
||||||
history_path.push("history.sqlite3");
|
}
|
||||||
}
|
|
||||||
HistoryFileFormat::Plaintext => {
|
|
||||||
history_path.push("history.txt");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if clear {
|
let long = call.has_flag(engine_state, stack, "long")?;
|
||||||
let _ = std::fs::remove_file(history_path);
|
let signals = engine_state.signals().clone();
|
||||||
// TODO: FIXME also clear the auxiliary files when using sqlite
|
let history_reader: Option<Box<dyn ReedlineHistory>> = match history.file_format {
|
||||||
Ok(PipelineData::empty())
|
HistoryFileFormat::Sqlite => {
|
||||||
} else {
|
SqliteBackedHistory::with_file(history_path.clone(), None, None)
|
||||||
let history_reader: Option<Box<dyn ReedlineHistory>> = match history.file_format {
|
|
||||||
HistoryFileFormat::Sqlite => {
|
|
||||||
SqliteBackedHistory::with_file(history_path.clone().into(), None, None)
|
|
||||||
.map(|inner| {
|
|
||||||
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
|
|
||||||
boxed
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryFileFormat::Plaintext => FileBackedHistory::with_file(
|
|
||||||
history.max_size as usize,
|
|
||||||
history_path.clone().into(),
|
|
||||||
)
|
|
||||||
.map(|inner| {
|
.map(|inner| {
|
||||||
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
|
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
|
||||||
boxed
|
boxed
|
||||||
})
|
})
|
||||||
.ok(),
|
.ok()
|
||||||
};
|
|
||||||
|
|
||||||
match history.file_format {
|
|
||||||
HistoryFileFormat::Plaintext => Ok(history_reader
|
|
||||||
.and_then(|h| {
|
|
||||||
h.search(SearchQuery::everything(SearchDirection::Forward, None))
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.map(move |entries| {
|
|
||||||
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
|
||||||
Value::record(
|
|
||||||
record! {
|
|
||||||
"command" => Value::string(entry.command_line, head),
|
|
||||||
"index" => Value::int(idx as i64, head),
|
|
||||||
},
|
|
||||||
head,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.ok_or(ShellError::FileNotFound {
|
|
||||||
file: history_path.display().to_string(),
|
|
||||||
span: head,
|
|
||||||
})?
|
|
||||||
.into_pipeline_data(head, signals)),
|
|
||||||
HistoryFileFormat::Sqlite => Ok(history_reader
|
|
||||||
.and_then(|h| {
|
|
||||||
h.search(SearchQuery::everything(SearchDirection::Forward, None))
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.map(move |entries| {
|
|
||||||
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
|
||||||
create_history_record(idx, entry, long, head)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.ok_or(ShellError::FileNotFound {
|
|
||||||
file: history_path.display().to_string(),
|
|
||||||
span: head,
|
|
||||||
})?
|
|
||||||
.into_pipeline_data(head, signals)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
HistoryFileFormat::Plaintext => {
|
||||||
Err(ShellError::ConfigDirNotFound { span: Some(head) })
|
FileBackedHistory::with_file(history.max_size as usize, history_path.clone())
|
||||||
|
.map(|inner| {
|
||||||
|
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
|
||||||
|
boxed
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match history.file_format {
|
||||||
|
HistoryFileFormat::Plaintext => Ok(history_reader
|
||||||
|
.and_then(|h| {
|
||||||
|
h.search(SearchQuery::everything(SearchDirection::Forward, None))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.map(move |entries| {
|
||||||
|
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
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,
|
||||||
|
})?
|
||||||
|
.into_pipeline_data(head, signals)),
|
||||||
|
HistoryFileFormat::Sqlite => Ok(history_reader
|
||||||
|
.and_then(|h| {
|
||||||
|
h.search(SearchQuery::everything(SearchDirection::Forward, None))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.map(move |entries| {
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(move |(idx, entry)| create_history_record(idx, entry, long, head))
|
||||||
|
})
|
||||||
|
.ok_or(ShellError::FileNotFound {
|
||||||
|
file: history_path.display().to_string(),
|
||||||
|
span: head,
|
||||||
|
})?
|
||||||
|
.into_pipeline_data(head, signals)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,13 +179,13 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span)
|
|||||||
Value::record(
|
Value::record(
|
||||||
record! {
|
record! {
|
||||||
"item_id" => item_id_value,
|
"item_id" => item_id_value,
|
||||||
"start_timestamp" => start_timestamp_value,
|
fields::START_TIMESTAMP => start_timestamp_value,
|
||||||
"command" => command_value,
|
fields::COMMAND_LINE => command_value,
|
||||||
"session_id" => session_id_value,
|
fields::SESSION_ID => session_id_value,
|
||||||
"hostname" => hostname_value,
|
fields::HOSTNAME => hostname_value,
|
||||||
"cwd" => cwd_value,
|
fields::CWD => cwd_value,
|
||||||
"duration" => duration_value,
|
fields::DURATION => duration_value,
|
||||||
"exit_status" => exit_status_value,
|
fields::EXIT_STATUS => exit_status_value,
|
||||||
"idx" => index_value,
|
"idx" => index_value,
|
||||||
},
|
},
|
||||||
head,
|
head,
|
||||||
@ -206,11 +193,11 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span)
|
|||||||
} else {
|
} else {
|
||||||
Value::record(
|
Value::record(
|
||||||
record! {
|
record! {
|
||||||
"start_timestamp" => start_timestamp_value,
|
fields::START_TIMESTAMP => start_timestamp_value,
|
||||||
"command" => command_value,
|
fields::COMMAND_LINE => command_value,
|
||||||
"cwd" => cwd_value,
|
fields::CWD => cwd_value,
|
||||||
"duration" => duration_value,
|
fields::DURATION => duration_value,
|
||||||
"exit_status" => exit_status_value,
|
fields::EXIT_STATUS => exit_status_value,
|
||||||
},
|
},
|
||||||
head,
|
head,
|
||||||
)
|
)
|
||||||
|
415
crates/nu-cli/src/commands/history/history_import.rs
Normal file
415
crates/nu-cli/src/commands/history/history_import.rs
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::HistoryFileFormat;
|
||||||
|
|
||||||
|
use reedline::{
|
||||||
|
FileBackedHistory, History, HistoryItem, ReedlineError, SearchQuery, SqliteBackedHistory,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::fields;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HistoryImport;
|
||||||
|
|
||||||
|
impl Command for HistoryImport {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"history import"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Import command line history"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"Can import history from input, either successive command lines or more detailed records. If providing records, available fields are:
|
||||||
|
command_line, id, start_timestamp, hostname, cwd, duration, exit_status.
|
||||||
|
|
||||||
|
If no input is provided, will import all history items from existing history in the other format: if current history is stored in sqlite, it will store it in plain text and vice versa.
|
||||||
|
|
||||||
|
Note that history item IDs are ignored when importing from file."#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("history import")
|
||||||
|
.category(Category::History)
|
||||||
|
.input_output_types(vec![
|
||||||
|
(Type::Nothing, Type::Nothing),
|
||||||
|
(Type::List(Box::new(Type::String)), Type::Nothing),
|
||||||
|
(Type::table(), Type::Nothing),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
example: "history import",
|
||||||
|
description:
|
||||||
|
"Append all items from history in the other format to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "echo foo | history import",
|
||||||
|
description: "Append `foo` to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "[[ command_line cwd ]; [ foo /home ]] | history import",
|
||||||
|
description: "Append `foo` ran from `/home` to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let ok = Ok(Value::nothing(call.head).into_pipeline_data());
|
||||||
|
|
||||||
|
let Some(history) = engine_state.history_config() else {
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
let Some(current_history_path) = history.file_path() else {
|
||||||
|
return Err(ShellError::ConfigDirNotFound {
|
||||||
|
span: Some(call.head),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if let Some(bak_path) = backup(¤t_history_path)? {
|
||||||
|
println!("Backed history to {}", bak_path.display());
|
||||||
|
}
|
||||||
|
match input {
|
||||||
|
PipelineData::Empty => {
|
||||||
|
let other_format = match history.file_format {
|
||||||
|
HistoryFileFormat::Sqlite => HistoryFileFormat::Plaintext,
|
||||||
|
HistoryFileFormat::Plaintext => HistoryFileFormat::Sqlite,
|
||||||
|
};
|
||||||
|
let src = new_backend(other_format, None)?;
|
||||||
|
let mut dst = new_backend(history.file_format, Some(current_history_path))?;
|
||||||
|
let items = src
|
||||||
|
.search(SearchQuery::everything(
|
||||||
|
reedline::SearchDirection::Forward,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.map_err(error_from_reedline)?
|
||||||
|
.into_iter()
|
||||||
|
.map(Ok);
|
||||||
|
import(dst.as_mut(), items)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let input = input.into_iter().map(item_from_value);
|
||||||
|
import(
|
||||||
|
new_backend(history.file_format, Some(current_history_path))?.as_mut(),
|
||||||
|
input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_backend(
|
||||||
|
format: HistoryFileFormat,
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
) -> Result<Box<dyn History>, ShellError> {
|
||||||
|
let path = match path {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
let Some(mut path) = nu_path::nu_config_dir() else {
|
||||||
|
return Err(ShellError::ConfigDirNotFound { span: None });
|
||||||
|
};
|
||||||
|
path.push(format.default_file_name());
|
||||||
|
path.into_std_path_buf()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn map(
|
||||||
|
result: Result<impl History + 'static, ReedlineError>,
|
||||||
|
) -> Result<Box<dyn History>, ShellError> {
|
||||||
|
result
|
||||||
|
.map(|x| Box::new(x) as Box<dyn History>)
|
||||||
|
.map_err(error_from_reedline)
|
||||||
|
}
|
||||||
|
match format {
|
||||||
|
// Use a reasonably large value for maximum capacity.
|
||||||
|
HistoryFileFormat::Plaintext => map(FileBackedHistory::with_file(0xfffffff, path)),
|
||||||
|
HistoryFileFormat::Sqlite => map(SqliteBackedHistory::with_file(path, None, None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import(
|
||||||
|
dst: &mut dyn History,
|
||||||
|
src: impl Iterator<Item = Result<HistoryItem, ShellError>>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
for item in src {
|
||||||
|
let mut item = item?;
|
||||||
|
item.id = None;
|
||||||
|
dst.save(item).map_err(error_from_reedline)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_from_reedline(e: ReedlineError) -> ShellError {
|
||||||
|
// TODO: Should we add a new ShellError variant?
|
||||||
|
ShellError::GenericError {
|
||||||
|
error: "Reedline error".to_owned(),
|
||||||
|
msg: format!("{e}"),
|
||||||
|
span: None,
|
||||||
|
help: None,
|
||||||
|
inner: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_from_value(v: Value) -> Result<HistoryItem, ShellError> {
|
||||||
|
let span = v.span();
|
||||||
|
match v {
|
||||||
|
Value::Record { val, .. } => item_from_record(val.into_owned(), span),
|
||||||
|
Value::String { val, .. } => Ok(HistoryItem {
|
||||||
|
command_line: val,
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None,
|
||||||
|
}),
|
||||||
|
_ => Err(ShellError::UnsupportedInput {
|
||||||
|
msg: "Only list and record inputs are supported".to_owned(),
|
||||||
|
input: v.get_type().to_string(),
|
||||||
|
msg_span: span,
|
||||||
|
input_span: span,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_from_record(mut rec: Record, span: Span) -> Result<HistoryItem, ShellError> {
|
||||||
|
let cmd = match rec.remove(fields::COMMAND_LINE) {
|
||||||
|
Some(v) => v.as_str()?.to_owned(),
|
||||||
|
None => {
|
||||||
|
return Err(ShellError::TypeMismatch {
|
||||||
|
err_message: format!("missing column: {}", fields::COMMAND_LINE),
|
||||||
|
span,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get<T>(
|
||||||
|
rec: &mut Record,
|
||||||
|
field: &'static str,
|
||||||
|
f: impl FnOnce(Value) -> Result<T, ShellError>,
|
||||||
|
) -> Result<Option<T>, ShellError> {
|
||||||
|
rec.remove(field).map(f).transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
let rec = &mut rec;
|
||||||
|
let item = HistoryItem {
|
||||||
|
command_line: cmd,
|
||||||
|
id: None,
|
||||||
|
start_timestamp: get(rec, fields::START_TIMESTAMP, |v| Ok(v.as_date()?.to_utc()))?,
|
||||||
|
hostname: get(rec, fields::HOSTNAME, |v| Ok(v.as_str()?.to_owned()))?,
|
||||||
|
cwd: get(rec, fields::CWD, |v| Ok(v.as_str()?.to_owned()))?,
|
||||||
|
exit_status: get(rec, fields::EXIT_STATUS, |v| v.as_int())?,
|
||||||
|
duration: get(rec, fields::DURATION, duration_from_value)?,
|
||||||
|
more_info: None,
|
||||||
|
// TODO: Currently reedline doesn't let you create session IDs.
|
||||||
|
session_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !rec.is_empty() {
|
||||||
|
let cols = rec.columns().map(|s| s.as_str()).collect::<Vec<_>>();
|
||||||
|
return Err(ShellError::TypeMismatch {
|
||||||
|
err_message: format!("unsupported column names: {}", cols.join(", ")),
|
||||||
|
span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duration_from_value(v: Value) -> Result<std::time::Duration, ShellError> {
|
||||||
|
chrono::Duration::nanoseconds(v.as_duration()?)
|
||||||
|
.to_std()
|
||||||
|
.map_err(|_| ShellError::IOError {
|
||||||
|
msg: "negative duration not supported".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_backup_path(path: &Path) -> Result<PathBuf, ShellError> {
|
||||||
|
let Ok(mut bak_path) = path.to_path_buf().into_os_string().into_string() else {
|
||||||
|
// This isn't fundamentally problem, but trying to work with OsString is a nightmare.
|
||||||
|
return Err(ShellError::IOError {
|
||||||
|
msg: "History path mush be representable as UTF-8".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
bak_path.push_str(".bak");
|
||||||
|
if !Path::new(&bak_path).exists() {
|
||||||
|
return Ok(bak_path.into());
|
||||||
|
}
|
||||||
|
let base_len = bak_path.len();
|
||||||
|
for i in 1..100 {
|
||||||
|
use std::fmt::Write;
|
||||||
|
bak_path.truncate(base_len);
|
||||||
|
write!(&mut bak_path, ".{i}").unwrap();
|
||||||
|
if !Path::new(&bak_path).exists() {
|
||||||
|
return Ok(PathBuf::from(bak_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ShellError::IOError {
|
||||||
|
msg: "Too many existing backup files".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backup(path: &Path) -> Result<Option<PathBuf>, ShellError> {
|
||||||
|
match path.metadata() {
|
||||||
|
Ok(md) if md.is_file() => (),
|
||||||
|
Ok(_) => {
|
||||||
|
return Err(ShellError::IOError {
|
||||||
|
msg: "history path exists but is not a file".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
}
|
||||||
|
let bak_path = find_backup_path(path)?;
|
||||||
|
std::fs::copy(path, &bak_path)?;
|
||||||
|
Ok(Some(bak_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::DateTime;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_string() -> Result<(), ShellError> {
|
||||||
|
let item = item_from_value(Value::string("foo", Span::unknown()))?;
|
||||||
|
assert_eq!(
|
||||||
|
item,
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command", Value::string("foo", span)),
|
||||||
|
(
|
||||||
|
"start_timestamp",
|
||||||
|
Value::date(
|
||||||
|
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap(),
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("hostname", Value::string("localhost", span)),
|
||||||
|
("cwd", Value::string("/home/test", span)),
|
||||||
|
("duration", Value::duration(100_000_000, span)),
|
||||||
|
("exit_status", Value::int(42, span)),
|
||||||
|
]);
|
||||||
|
let item = item_from_value(rec).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
item,
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: Some(
|
||||||
|
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc()
|
||||||
|
),
|
||||||
|
hostname: Some("localhost".to_string()),
|
||||||
|
cwd: Some("/home/test".to_string()),
|
||||||
|
duration: Some(std::time::Duration::from_nanos(100_000_000)),
|
||||||
|
exit_status: Some(42),
|
||||||
|
|
||||||
|
session_id: None,
|
||||||
|
more_info: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record_extra_field() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command_line", Value::string("foo", span)),
|
||||||
|
("id_nonexistent", Value::int(1, span)),
|
||||||
|
]);
|
||||||
|
assert!(item_from_value(rec).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record_bad_type() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command_line", Value::string("foo", span)),
|
||||||
|
("id", Value::string("one".to_string(), span)),
|
||||||
|
]);
|
||||||
|
assert!(item_from_value(rec).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_record(rec: &[(&'static str, Value)]) -> Value {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = Record::from_raw_cols_vals(
|
||||||
|
rec.iter().map(|(col, _)| col.to_string()).collect(),
|
||||||
|
rec.iter().map(|(_, val)| val.clone()).collect(),
|
||||||
|
span,
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
Value::record(rec, span)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::no_backup(&["history.dat"], "history.dat.bak")]
|
||||||
|
#[case::backup_exists(&["history.dat", "history.dat.bak"], "history.dat.bak.1")]
|
||||||
|
#[case::multiple_backups_exists( &["history.dat", "history.dat.bak", "history.dat.bak.1"], "history.dat.bak.2")]
|
||||||
|
fn test_find_backup_path(#[case] existing: &[&str], #[case] want: &str) {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
for name in existing {
|
||||||
|
std::fs::File::create_new(dir.path().join(name)).unwrap();
|
||||||
|
}
|
||||||
|
let got = find_backup_path(&dir.path().join("history.dat")).unwrap();
|
||||||
|
assert_eq!(got, dir.path().join(want))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut history = std::fs::File::create_new(dir.path().join("history.dat")).unwrap();
|
||||||
|
use std::io::Write;
|
||||||
|
write!(&mut history, "123").unwrap();
|
||||||
|
let want_bak_path = dir.path().join("history.dat.bak");
|
||||||
|
assert_eq!(
|
||||||
|
backup(&dir.path().join("history.dat")),
|
||||||
|
Ok(Some(want_bak_path.clone()))
|
||||||
|
);
|
||||||
|
let got_data = String::from_utf8(std::fs::read(want_bak_path).unwrap()).unwrap();
|
||||||
|
assert_eq!(got_data, "123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup_no_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let bak_path = backup(&dir.path().join("history.dat")).unwrap();
|
||||||
|
assert!(bak_path.is_none());
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
|
mod fields;
|
||||||
mod history_;
|
mod history_;
|
||||||
|
mod history_import;
|
||||||
mod history_session;
|
mod history_session;
|
||||||
|
|
||||||
pub use history_::History;
|
pub use history_::History;
|
||||||
|
pub use history_import::HistoryImport;
|
||||||
pub use history_session::HistorySession;
|
pub use history_session::HistorySession;
|
||||||
|
@ -7,7 +7,7 @@ mod keybindings_list;
|
|||||||
mod keybindings_listen;
|
mod keybindings_listen;
|
||||||
|
|
||||||
pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor};
|
pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor};
|
||||||
pub use history::{History, HistorySession};
|
pub use history::{History, HistoryImport, HistorySession};
|
||||||
pub use keybindings::Keybindings;
|
pub use keybindings::Keybindings;
|
||||||
pub use keybindings_default::KeybindingsDefault;
|
pub use keybindings_default::KeybindingsDefault;
|
||||||
pub use keybindings_list::KeybindingsList;
|
pub use keybindings_list::KeybindingsList;
|
||||||
|
@ -12,7 +12,7 @@ pub trait Completer {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
|
@ -158,7 +158,7 @@ impl Completer for CommandCompletion {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
_stack: &Stack,
|
_stack: &Stack,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
@ -195,7 +195,7 @@ impl Completer for CommandCompletion {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !subcommands.is_empty() {
|
if !subcommands.is_empty() {
|
||||||
return sort_suggestions(&String::from_utf8_lossy(&prefix), subcommands, options);
|
return sort_suggestions(&String::from_utf8_lossy(prefix), subcommands, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = working_set.get_config();
|
let config = working_set.get_config();
|
||||||
@ -220,7 +220,7 @@ impl Completer for CommandCompletion {
|
|||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
sort_suggestions(&String::from_utf8_lossy(&prefix), commands, options)
|
sort_suggestions(&String::from_utf8_lossy(prefix), commands, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::completions::{
|
use crate::completions::{
|
||||||
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
|
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
|
||||||
DotNuCompletion, FileCompletion, FlagCompletion, VariableCompletion,
|
DotNuCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion,
|
||||||
};
|
};
|
||||||
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
||||||
use nu_engine::eval_block;
|
use nu_engine::eval_block;
|
||||||
@ -25,7 +25,7 @@ impl NuCompleter {
|
|||||||
pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
|
pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
engine_state,
|
engine_state,
|
||||||
stack: Stack::with_parent(stack).reset_out_dest().capture(),
|
stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ impl NuCompleter {
|
|||||||
&self,
|
&self,
|
||||||
completer: &mut T,
|
completer: &mut T,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
new_span: Span,
|
new_span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
@ -55,7 +55,7 @@ impl NuCompleter {
|
|||||||
completer.fetch(
|
completer.fetch(
|
||||||
working_set,
|
working_set,
|
||||||
&self.stack,
|
&self.stack,
|
||||||
prefix.clone(),
|
prefix,
|
||||||
new_span,
|
new_span,
|
||||||
offset,
|
offset,
|
||||||
pos,
|
pos,
|
||||||
@ -170,23 +170,38 @@ impl NuCompleter {
|
|||||||
let new_span = Span::new(flat.0.start, flat.0.end - 1);
|
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.
|
// Parses the prefix. Completion should look up to the cursor position, not after.
|
||||||
let mut prefix = working_set.get_span_contents(flat.0).to_vec();
|
let mut prefix = working_set.get_span_contents(flat.0);
|
||||||
let index = pos - flat.0.start;
|
let index = pos - flat.0.start;
|
||||||
prefix.drain(index..);
|
prefix = &prefix[..index];
|
||||||
|
|
||||||
// Variables completion
|
// Variables completion
|
||||||
if prefix.starts_with(b"$") || most_left_var.is_some() {
|
if prefix.starts_with(b"$") || most_left_var.is_some() {
|
||||||
let mut completer =
|
let mut variable_names_completer =
|
||||||
VariableCompletion::new(most_left_var.unwrap_or((vec![], vec![])));
|
VariableCompletion::new(most_left_var.unwrap_or((vec![], vec![])));
|
||||||
|
|
||||||
return self.process_completion(
|
let mut variable_completions = self.process_completion(
|
||||||
&mut completer,
|
&mut variable_names_completer,
|
||||||
&working_set,
|
&working_set,
|
||||||
prefix,
|
prefix,
|
||||||
new_span,
|
new_span,
|
||||||
fake_offset,
|
fake_offset,
|
||||||
pos,
|
pos,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut variable_operations_completer =
|
||||||
|
OperatorCompletion::new(pipeline_element.expr.clone());
|
||||||
|
|
||||||
|
let mut variable_operations_completions = self.process_completion(
|
||||||
|
&mut variable_operations_completer,
|
||||||
|
&working_set,
|
||||||
|
prefix,
|
||||||
|
new_span,
|
||||||
|
fake_offset,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
|
||||||
|
variable_completions.append(&mut variable_operations_completions);
|
||||||
|
return variable_completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flags completion
|
// Flags completion
|
||||||
@ -196,7 +211,7 @@ impl NuCompleter {
|
|||||||
let result = self.process_completion(
|
let result = self.process_completion(
|
||||||
&mut completer,
|
&mut completer,
|
||||||
&working_set,
|
&working_set,
|
||||||
prefix.clone(),
|
prefix,
|
||||||
new_span,
|
new_span,
|
||||||
fake_offset,
|
fake_offset,
|
||||||
pos,
|
pos,
|
||||||
@ -262,6 +277,26 @@ impl NuCompleter {
|
|||||||
} else if prev_expr_str == b"ls" {
|
} else if prev_expr_str == b"ls" {
|
||||||
let mut completer = FileCompletion::new();
|
let mut completer = FileCompletion::new();
|
||||||
|
|
||||||
|
return self.process_completion(
|
||||||
|
&mut completer,
|
||||||
|
&working_set,
|
||||||
|
prefix,
|
||||||
|
new_span,
|
||||||
|
fake_offset,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
} else if matches!(
|
||||||
|
previous_expr.1,
|
||||||
|
FlatShape::Float
|
||||||
|
| FlatShape::Int
|
||||||
|
| FlatShape::String
|
||||||
|
| FlatShape::List
|
||||||
|
| FlatShape::Bool
|
||||||
|
| FlatShape::Variable(_)
|
||||||
|
) {
|
||||||
|
let mut completer =
|
||||||
|
OperatorCompletion::new(pipeline_element.expr.clone());
|
||||||
|
|
||||||
return self.process_completion(
|
return self.process_completion(
|
||||||
&mut completer,
|
&mut completer,
|
||||||
&working_set,
|
&working_set,
|
||||||
@ -327,7 +362,7 @@ impl NuCompleter {
|
|||||||
let mut out: Vec<_> = self.process_completion(
|
let mut out: Vec<_> = self.process_completion(
|
||||||
&mut completer,
|
&mut completer,
|
||||||
&working_set,
|
&working_set,
|
||||||
prefix.clone(),
|
prefix,
|
||||||
new_span,
|
new_span,
|
||||||
fake_offset,
|
fake_offset,
|
||||||
pos,
|
pos,
|
||||||
@ -533,6 +568,11 @@ mod completer_tests {
|
|||||||
|
|
||||||
let mut completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
|
let mut completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
|
||||||
let dataset = [
|
let dataset = [
|
||||||
|
("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
|
||||||
|
("1.0 bit-sh", false, "b", vec![]),
|
||||||
|
("1 m", true, "m", vec!["mod"]),
|
||||||
|
("1.0 m", true, "m", vec!["mod"]),
|
||||||
|
("\"a\" s", true, "s", vec!["starts-with"]),
|
||||||
("sudo", false, "", Vec::new()),
|
("sudo", false, "", Vec::new()),
|
||||||
("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
|
("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
|
||||||
(" sudo", false, "", Vec::new()),
|
(" sudo", false, "", Vec::new()),
|
||||||
|
@ -180,7 +180,7 @@ pub fn complete_item(
|
|||||||
&& engine_state.config.use_ansi_coloring)
|
&& engine_state.config.use_ansi_coloring)
|
||||||
.then(|| {
|
.then(|| {
|
||||||
let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
|
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(),
|
Some(v) => env_to_string("LS_COLORS", v, engine_state, stack).ok(),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
get_ls_colors(ls_colors_env_str)
|
get_ls_colors(ls_colors_env_str)
|
||||||
@ -267,8 +267,10 @@ pub fn escape_path(path: String, dir: bool) -> String {
|
|||||||
let filename_contaminated = !dir && path.contains(['\'', '"', ' ', '#', '(', ')']);
|
let filename_contaminated = !dir && path.contains(['\'', '"', ' ', '#', '(', ')']);
|
||||||
let dirname_contaminated = dir && path.contains(['\'', '"', ' ', '#']);
|
let dirname_contaminated = dir && path.contains(['\'', '"', ' ', '#']);
|
||||||
let maybe_flag = path.starts_with('-');
|
let maybe_flag = path.starts_with('-');
|
||||||
|
let maybe_variable = path.starts_with('$');
|
||||||
let maybe_number = path.parse::<f64>().is_ok();
|
let maybe_number = path.parse::<f64>().is_ok();
|
||||||
if filename_contaminated || dirname_contaminated || maybe_flag || maybe_number {
|
if filename_contaminated || dirname_contaminated || maybe_flag || maybe_variable || maybe_number
|
||||||
|
{
|
||||||
format!("`{path}`")
|
format!("`{path}`")
|
||||||
} else {
|
} else {
|
||||||
path
|
path
|
||||||
@ -333,7 +335,7 @@ pub fn sort_completions<T>(
|
|||||||
} else {
|
} else {
|
||||||
matcher = matcher.ignore_case();
|
matcher = matcher.ignore_case();
|
||||||
};
|
};
|
||||||
items.sort_by(|a, b| {
|
items.sort_unstable_by(|a, b| {
|
||||||
let a_str = get_value(a);
|
let a_str = get_value(a);
|
||||||
let b_str = get_value(b);
|
let b_str = get_value(b);
|
||||||
let a_score = matcher.fuzzy_match(a_str, prefix).unwrap_or_default();
|
let a_score = matcher.fuzzy_match(a_str, prefix).unwrap_or_default();
|
||||||
@ -341,7 +343,7 @@ pub fn sort_completions<T>(
|
|||||||
b_score.cmp(&a_score).then(a_str.cmp(b_str))
|
b_score.cmp(&a_score).then(a_str.cmp(b_str))
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.sort_by(|a, b| get_value(a).cmp(get_value(b)));
|
items.sort_unstable_by(|a, b| get_value(a).cmp(get_value(b)));
|
||||||
}
|
}
|
||||||
|
|
||||||
items
|
items
|
||||||
|
@ -7,7 +7,7 @@ use nu_protocol::{
|
|||||||
ast::{Argument, Call, Expr, Expression},
|
ast::{Argument, Call, Expr, Expression},
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
CompletionSort, PipelineData, Span, Type, Value,
|
CompletionSort, DeclId, PipelineData, Span, Type, Value,
|
||||||
};
|
};
|
||||||
use nu_utils::IgnoreCaseExt;
|
use nu_utils::IgnoreCaseExt;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -16,12 +16,12 @@ use super::completion_common::sort_suggestions;
|
|||||||
|
|
||||||
pub struct CustomCompletion {
|
pub struct CustomCompletion {
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
decl_id: usize,
|
decl_id: DeclId,
|
||||||
line: String,
|
line: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomCompletion {
|
impl CustomCompletion {
|
||||||
pub fn new(stack: Stack, decl_id: usize, line: String) -> Self {
|
pub fn new(stack: Stack, decl_id: DeclId, line: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stack,
|
stack,
|
||||||
decl_id,
|
decl_id,
|
||||||
@ -35,7 +35,7 @@ impl Completer for CustomCompletion {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
_stack: &Stack,
|
_stack: &Stack,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
@ -126,8 +126,8 @@ impl Completer for CustomCompletion {
|
|||||||
let options = custom_completion_options
|
let options = custom_completion_options
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(completion_options);
|
.unwrap_or(completion_options);
|
||||||
let suggestions = filter(&prefix, suggestions, options);
|
let suggestions = filter(prefix, suggestions, options);
|
||||||
sort_suggestions(&String::from_utf8_lossy(&prefix), suggestions, options)
|
sort_suggestions(&String::from_utf8_lossy(prefix), suggestions, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,13 +26,13 @@ impl Completer for DirectoryCompletion {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
_pos: usize,
|
_pos: usize,
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
let AdjustView { prefix, span, .. } = adjust_if_intermediate(&prefix, working_set, span);
|
let AdjustView { prefix, span, .. } = adjust_if_intermediate(prefix, working_set, span);
|
||||||
|
|
||||||
// Filter only the folders
|
// Filter only the folders
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
|
@ -22,13 +22,13 @@ impl Completer for DotNuCompletion {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
_pos: usize,
|
_pos: usize,
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
let prefix_str = String::from_utf8_lossy(&prefix).replace('`', "");
|
let prefix_str = String::from_utf8_lossy(prefix).replace('`', "");
|
||||||
let mut search_dirs: Vec<String> = vec![];
|
let mut search_dirs: Vec<String> = vec![];
|
||||||
|
|
||||||
// If prefix_str is only a word we want to search in the current dir
|
// If prefix_str is only a word we want to search in the current dir
|
||||||
|
@ -27,7 +27,7 @@ impl Completer for FileCompletion {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
_pos: usize,
|
_pos: usize,
|
||||||
@ -37,7 +37,7 @@ impl Completer for FileCompletion {
|
|||||||
prefix,
|
prefix,
|
||||||
span,
|
span,
|
||||||
readjusted,
|
readjusted,
|
||||||
} = adjust_if_intermediate(&prefix, working_set, span);
|
} = adjust_if_intermediate(prefix, working_set, span);
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let items: Vec<_> = complete_item(
|
let items: Vec<_> = complete_item(
|
||||||
|
@ -24,7 +24,7 @@ impl Completer for FlagCompletion {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
_stack: &Stack,
|
_stack: &Stack,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
_pos: usize,
|
_pos: usize,
|
||||||
@ -44,7 +44,7 @@ impl Completer for FlagCompletion {
|
|||||||
short.encode_utf8(&mut named);
|
short.encode_utf8(&mut named);
|
||||||
named.insert(0, b'-');
|
named.insert(0, b'-');
|
||||||
|
|
||||||
if options.match_algorithm.matches_u8(&named, &prefix) {
|
if options.match_algorithm.matches_u8(&named, prefix) {
|
||||||
output.push(SemanticSuggestion {
|
output.push(SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(&named).to_string(),
|
value: String::from_utf8_lossy(&named).to_string(),
|
||||||
@ -70,7 +70,7 @@ impl Completer for FlagCompletion {
|
|||||||
named.insert(0, b'-');
|
named.insert(0, b'-');
|
||||||
named.insert(0, b'-');
|
named.insert(0, b'-');
|
||||||
|
|
||||||
if options.match_algorithm.matches_u8(&named, &prefix) {
|
if options.match_algorithm.matches_u8(&named, prefix) {
|
||||||
output.push(SemanticSuggestion {
|
output.push(SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
value: String::from_utf8_lossy(&named).to_string(),
|
value: String::from_utf8_lossy(&named).to_string(),
|
||||||
@ -88,7 +88,7 @@ impl Completer for FlagCompletion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sort_suggestions(&String::from_utf8_lossy(&prefix), output, options);
|
return sort_suggestions(&String::from_utf8_lossy(prefix), output, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
vec![]
|
vec![]
|
||||||
|
@ -8,6 +8,7 @@ mod directory_completions;
|
|||||||
mod dotnu_completions;
|
mod dotnu_completions;
|
||||||
mod file_completions;
|
mod file_completions;
|
||||||
mod flag_completions;
|
mod flag_completions;
|
||||||
|
mod operator_completions;
|
||||||
mod variable_completions;
|
mod variable_completions;
|
||||||
|
|
||||||
pub use base::{Completer, SemanticSuggestion, SuggestionKind};
|
pub use base::{Completer, SemanticSuggestion, SuggestionKind};
|
||||||
@ -19,4 +20,5 @@ pub use directory_completions::DirectoryCompletion;
|
|||||||
pub use dotnu_completions::DotNuCompletion;
|
pub use dotnu_completions::DotNuCompletion;
|
||||||
pub use file_completions::{file_path_completion, matches, FileCompletion};
|
pub use file_completions::{file_path_completion, matches, FileCompletion};
|
||||||
pub use flag_completions::FlagCompletion;
|
pub use flag_completions::FlagCompletion;
|
||||||
|
pub use operator_completions::OperatorCompletion;
|
||||||
pub use variable_completions::VariableCompletion;
|
pub use variable_completions::VariableCompletion;
|
||||||
|
182
crates/nu-cli/src/completions/operator_completions.rs
Normal file
182
crates/nu-cli/src/completions/operator_completions.rs
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
ast::{Expr, Expression},
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
Span, Type,
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OperatorCompletion {
|
||||||
|
previous_expr: Expression,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperatorCompletion {
|
||||||
|
pub fn new(previous_expr: Expression) -> Self {
|
||||||
|
OperatorCompletion { previous_expr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for OperatorCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
_stack: &Stack,
|
||||||
|
_prefix: &[u8],
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
_pos: usize,
|
||||||
|
_options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
//Check if int, float, or string
|
||||||
|
let partial = std::str::from_utf8(working_set.get_span_contents(span)).unwrap_or("");
|
||||||
|
let op = match &self.previous_expr.expr {
|
||||||
|
Expr::BinaryOp(x, _, _) => &x.expr,
|
||||||
|
_ => {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let possible_operations = match op {
|
||||||
|
Expr::Int(_) => vec![
|
||||||
|
("+", "Add (Plus)"),
|
||||||
|
("-", "Subtract (Minus)"),
|
||||||
|
("*", "Multiply"),
|
||||||
|
("/", "Divide"),
|
||||||
|
("==", "Equal to"),
|
||||||
|
("!=", "Not equal to"),
|
||||||
|
("//", "Floor division"),
|
||||||
|
("<", "Less than"),
|
||||||
|
(">", "Greater than"),
|
||||||
|
("<=", "Less than or equal to"),
|
||||||
|
(">=", "Greater than or equal to"),
|
||||||
|
("mod", "Floor division remainder (Modulo)"),
|
||||||
|
("**", "Power of"),
|
||||||
|
("bit-or", "Bitwise OR"),
|
||||||
|
("bit-xor", "Bitwise exclusive OR"),
|
||||||
|
("bit-and", "Bitwise AND"),
|
||||||
|
("bit-shl", "Bitwise shift left"),
|
||||||
|
("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",
|
||||||
|
),
|
||||||
|
("in", "Is a member of (doesn't use regex)"),
|
||||||
|
("not-in", "Is not a member of (doesn't use regex)"),
|
||||||
|
("starts-with", "Starts with"),
|
||||||
|
("ends-with", "Ends with"),
|
||||||
|
],
|
||||||
|
Expr::Float(_) => vec![
|
||||||
|
("+", "Add (Plus)"),
|
||||||
|
("-", "Subtract (Minus)"),
|
||||||
|
("*", "Multiply"),
|
||||||
|
("/", "Divide"),
|
||||||
|
("==", "Equal to"),
|
||||||
|
("!=", "Not equal to"),
|
||||||
|
("//", "Floor division"),
|
||||||
|
("<", "Less than"),
|
||||||
|
(">", "Greater than"),
|
||||||
|
("<=", "Less than or equal to"),
|
||||||
|
(">=", "Greater than or equal to"),
|
||||||
|
("mod", "Floor division remainder (Modulo)"),
|
||||||
|
("**", "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![
|
||||||
|
(
|
||||||
|
"and",
|
||||||
|
"Both values are true (short-circuits when first value is false)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"or",
|
||||||
|
"Either value is true (short-circuits when first value is true)",
|
||||||
|
),
|
||||||
|
("xor", "One value is true and the other is false"),
|
||||||
|
("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::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 {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: x.0.to_string(),
|
||||||
|
description: Some(x.1.to_string()),
|
||||||
|
span: reedline::Span::new(span.start - offset, span.end - offset),
|
||||||
|
append_whitespace: true,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Command(
|
||||||
|
nu_protocol::engine::CommandType::Builtin,
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_variable_completions<'a>(
|
||||||
|
id: nu_protocol::Id<nu_protocol::marker::Var>,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
) -> Vec<(&'a str, &'a str)> {
|
||||||
|
let var = working_set.get_variable(id);
|
||||||
|
if !var.mutable {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
match var.ty {
|
||||||
|
Type::List(_) | Type::String | Type::Binary => vec![
|
||||||
|
(
|
||||||
|
"++=",
|
||||||
|
"Appends a list, a value, a string, or a binary value to a variable.",
|
||||||
|
),
|
||||||
|
("=", "Assigns a value to a variable."),
|
||||||
|
],
|
||||||
|
|
||||||
|
Type::Int | Type::Float => vec![
|
||||||
|
("=", "Assigns a value to a variable."),
|
||||||
|
("+=", "Adds a value to a variable."),
|
||||||
|
("-=", "Subtracts a value from a variable."),
|
||||||
|
("*=", "Multiplies a variable by a value"),
|
||||||
|
("/=", "Divides a variable by a value."),
|
||||||
|
],
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ impl Completer for VariableCompletion {
|
|||||||
&mut self,
|
&mut self,
|
||||||
working_set: &StateWorkingSet,
|
working_set: &StateWorkingSet,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
prefix: Vec<u8>,
|
prefix: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
_pos: usize,
|
_pos: usize,
|
||||||
@ -42,7 +42,7 @@ impl Completer for VariableCompletion {
|
|||||||
end: span.end - offset,
|
end: span.end - offset,
|
||||||
};
|
};
|
||||||
let sublevels_count = self.var_context.1.len();
|
let sublevels_count = self.var_context.1.len();
|
||||||
let prefix_str = String::from_utf8_lossy(&prefix);
|
let prefix_str = String::from_utf8_lossy(prefix);
|
||||||
|
|
||||||
// Completions for the given variable
|
// Completions for the given variable
|
||||||
if !var_str.is_empty() {
|
if !var_str.is_empty() {
|
||||||
@ -66,7 +66,7 @@ impl Completer for VariableCompletion {
|
|||||||
if options.match_algorithm.matches_u8_insensitive(
|
if options.match_algorithm.matches_u8_insensitive(
|
||||||
options.case_sensitive,
|
options.case_sensitive,
|
||||||
suggestion.suggestion.value.as_bytes(),
|
suggestion.suggestion.value.as_bytes(),
|
||||||
&prefix,
|
prefix,
|
||||||
) {
|
) {
|
||||||
output.push(suggestion);
|
output.push(suggestion);
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ impl Completer for VariableCompletion {
|
|||||||
if options.match_algorithm.matches_u8_insensitive(
|
if options.match_algorithm.matches_u8_insensitive(
|
||||||
options.case_sensitive,
|
options.case_sensitive,
|
||||||
env_var.0.as_bytes(),
|
env_var.0.as_bytes(),
|
||||||
&prefix,
|
prefix,
|
||||||
) {
|
) {
|
||||||
output.push(SemanticSuggestion {
|
output.push(SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
@ -111,7 +111,7 @@ impl Completer for VariableCompletion {
|
|||||||
if options.match_algorithm.matches_u8_insensitive(
|
if options.match_algorithm.matches_u8_insensitive(
|
||||||
options.case_sensitive,
|
options.case_sensitive,
|
||||||
suggestion.suggestion.value.as_bytes(),
|
suggestion.suggestion.value.as_bytes(),
|
||||||
&prefix,
|
prefix,
|
||||||
) {
|
) {
|
||||||
output.push(suggestion);
|
output.push(suggestion);
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ impl Completer for VariableCompletion {
|
|||||||
if options.match_algorithm.matches_u8_insensitive(
|
if options.match_algorithm.matches_u8_insensitive(
|
||||||
options.case_sensitive,
|
options.case_sensitive,
|
||||||
suggestion.suggestion.value.as_bytes(),
|
suggestion.suggestion.value.as_bytes(),
|
||||||
&prefix,
|
prefix,
|
||||||
) {
|
) {
|
||||||
output.push(suggestion);
|
output.push(suggestion);
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ impl Completer for VariableCompletion {
|
|||||||
if options.match_algorithm.matches_u8_insensitive(
|
if options.match_algorithm.matches_u8_insensitive(
|
||||||
options.case_sensitive,
|
options.case_sensitive,
|
||||||
builtin.as_bytes(),
|
builtin.as_bytes(),
|
||||||
&prefix,
|
prefix,
|
||||||
) {
|
) {
|
||||||
output.push(SemanticSuggestion {
|
output.push(SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
@ -173,7 +173,7 @@ impl Completer for VariableCompletion {
|
|||||||
if options.match_algorithm.matches_u8_insensitive(
|
if options.match_algorithm.matches_u8_insensitive(
|
||||||
options.case_sensitive,
|
options.case_sensitive,
|
||||||
v.0,
|
v.0,
|
||||||
&prefix,
|
prefix,
|
||||||
) {
|
) {
|
||||||
output.push(SemanticSuggestion {
|
output.push(SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
@ -201,7 +201,7 @@ impl Completer for VariableCompletion {
|
|||||||
if options.match_algorithm.matches_u8_insensitive(
|
if options.match_algorithm.matches_u8_insensitive(
|
||||||
options.case_sensitive,
|
options.case_sensitive,
|
||||||
v.0,
|
v.0,
|
||||||
&prefix,
|
prefix,
|
||||||
) {
|
) {
|
||||||
output.push(SemanticSuggestion {
|
output.push(SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
|
@ -5,7 +5,7 @@ use nu_path::canonicalize_with;
|
|||||||
use nu_protocol::{engine::StateWorkingSet, ParseError, PluginRegistryFile, Spanned};
|
use nu_protocol::{engine::StateWorkingSet, ParseError, PluginRegistryFile, Spanned};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack},
|
engine::{EngineState, Stack},
|
||||||
report_shell_error, HistoryFileFormat, PipelineData,
|
report_shell_error, PipelineData,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
use nu_utils::perf;
|
use nu_utils::perf;
|
||||||
@ -16,15 +16,8 @@ const PLUGIN_FILE: &str = "plugin.msgpackz";
|
|||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
const OLD_PLUGIN_FILE: &str = "plugin.nu";
|
const OLD_PLUGIN_FILE: &str = "plugin.nu";
|
||||||
|
|
||||||
const HISTORY_FILE_TXT: &str = "history.txt";
|
|
||||||
const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
|
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub fn read_plugin_file(
|
pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) {
|
||||||
engine_state: &mut EngineState,
|
|
||||||
plugin_file: Option<Spanned<String>>,
|
|
||||||
storage_path: &str,
|
|
||||||
) {
|
|
||||||
use nu_protocol::ShellError;
|
use nu_protocol::ShellError;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@ -52,7 +45,7 @@ pub fn read_plugin_file(
|
|||||||
let mut start_time = std::time::Instant::now();
|
let mut start_time = std::time::Instant::now();
|
||||||
// Reading signatures from plugin registry file
|
// Reading signatures from plugin registry file
|
||||||
// The plugin.msgpackz file stores the parsed signature collected from each registered plugin
|
// The plugin.msgpackz file stores the parsed signature collected from each registered plugin
|
||||||
add_plugin_file(engine_state, plugin_file.clone(), storage_path);
|
add_plugin_file(engine_state, plugin_file.clone());
|
||||||
perf!(
|
perf!(
|
||||||
"add plugin file to engine_state",
|
"add plugin file to engine_state",
|
||||||
start_time,
|
start_time,
|
||||||
@ -70,8 +63,7 @@ pub fn read_plugin_file(
|
|||||||
log::warn!("Plugin file not found: {}", plugin_path.display());
|
log::warn!("Plugin file not found: {}", plugin_path.display());
|
||||||
|
|
||||||
// Try migration of an old plugin file if this wasn't a custom plugin file
|
// Try migration of an old plugin file if this wasn't a custom plugin file
|
||||||
if plugin_file.is_none() && migrate_old_plugin_file(engine_state, storage_path)
|
if plugin_file.is_none() && migrate_old_plugin_file(engine_state) {
|
||||||
{
|
|
||||||
let Ok(file) = std::fs::File::open(&plugin_path) else {
|
let Ok(file) = std::fs::File::open(&plugin_path) else {
|
||||||
log::warn!("Failed to load newly migrated plugin file");
|
log::warn!("Failed to load newly migrated plugin file");
|
||||||
return;
|
return;
|
||||||
@ -159,11 +151,7 @@ pub fn read_plugin_file(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub fn add_plugin_file(
|
pub fn add_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) {
|
||||||
engine_state: &mut EngineState,
|
|
||||||
plugin_file: Option<Spanned<String>>,
|
|
||||||
storage_path: &str,
|
|
||||||
) {
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use nu_protocol::report_parse_error;
|
use nu_protocol::report_parse_error;
|
||||||
@ -189,9 +177,8 @@ pub fn add_plugin_file(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if let Some(mut plugin_path) = nu_path::config_dir() {
|
} else if let Some(plugin_path) = nu_path::nu_config_dir() {
|
||||||
// Path to store plugins signatures
|
// Path to store plugins signatures
|
||||||
plugin_path.push(storage_path);
|
|
||||||
let mut plugin_path =
|
let mut plugin_path =
|
||||||
canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path.into());
|
canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path.into());
|
||||||
plugin_path.push(PLUGIN_FILE);
|
plugin_path.push(PLUGIN_FILE);
|
||||||
@ -228,33 +215,15 @@ pub fn eval_config_contents(
|
|||||||
engine_state.file = prev_file;
|
engine_state.file = prev_file;
|
||||||
|
|
||||||
// Merge the environment in case env vars changed in the config
|
// Merge the environment in case env vars changed in the config
|
||||||
match engine_state.cwd(Some(stack)) {
|
if let Err(e) = engine_state.merge_env(stack) {
|
||||||
Ok(cwd) => {
|
report_shell_error(engine_state, &e);
|
||||||
if let Err(e) = engine_state.merge_env(stack, cwd) {
|
|
||||||
report_shell_error(engine_state, &e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
report_shell_error(engine_state, &e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_history_path(storage_path: &str, mode: HistoryFileFormat) -> Option<PathBuf> {
|
|
||||||
nu_path::config_dir().map(|mut history_path| {
|
|
||||||
history_path.push(storage_path);
|
|
||||||
history_path.push(match mode {
|
|
||||||
HistoryFileFormat::Plaintext => HISTORY_FILE_TXT,
|
|
||||||
HistoryFileFormat::Sqlite => HISTORY_FILE_SQLITE,
|
|
||||||
});
|
|
||||||
history_path.into()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -> bool {
|
pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool {
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
PluginExample, PluginIdentity, PluginRegistryItem, PluginRegistryItemData, PluginSignature,
|
PluginExample, PluginIdentity, PluginRegistryItem, PluginRegistryItemData, PluginSignature,
|
||||||
ShellError,
|
ShellError,
|
||||||
@ -267,10 +236,9 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(config_dir) = nu_path::config_dir().and_then(|mut dir| {
|
let Some(config_dir) =
|
||||||
dir.push(storage_path);
|
nu_path::nu_config_dir().and_then(|dir| nu_path::canonicalize_with(dir, &cwd).ok())
|
||||||
nu_path::canonicalize_with(dir, &cwd).ok()
|
else {
|
||||||
}) else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ pub use config_files::eval_config_contents;
|
|||||||
pub use eval_cmds::{evaluate_commands, EvaluateCommandsOpts};
|
pub use eval_cmds::{evaluate_commands, EvaluateCommandsOpts};
|
||||||
pub use eval_file::evaluate_file;
|
pub use eval_file::evaluate_file;
|
||||||
pub use menus::NuHelpCompleter;
|
pub use menus::NuHelpCompleter;
|
||||||
pub use nu_cmd_base::util::get_init_cwd;
|
|
||||||
pub use nu_highlight::NuHighlight;
|
pub use nu_highlight::NuHighlight;
|
||||||
pub use print::Print;
|
pub use print::Print;
|
||||||
pub use prompt::NushellPrompt;
|
pub use prompt::NushellPrompt;
|
||||||
|
@ -2,7 +2,7 @@ use nu_engine::eval_block;
|
|||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
engine::{EngineState, Stack},
|
engine::{EngineState, Stack},
|
||||||
IntoPipelineData, Span, Value,
|
BlockId, IntoPipelineData, Span, Value,
|
||||||
};
|
};
|
||||||
use reedline::{menu_functions::parse_selection_char, Completer, Suggestion};
|
use reedline::{menu_functions::parse_selection_char, Completer, Suggestion};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -10,7 +10,7 @@ use std::sync::Arc;
|
|||||||
const SELECTION_CHAR: char = '!';
|
const SELECTION_CHAR: char = '!';
|
||||||
|
|
||||||
pub struct NuMenuCompleter {
|
pub struct NuMenuCompleter {
|
||||||
block_id: usize,
|
block_id: BlockId,
|
||||||
span: Span,
|
span: Span,
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
engine_state: Arc<EngineState>,
|
engine_state: Arc<EngineState>,
|
||||||
@ -19,7 +19,7 @@ pub struct NuMenuCompleter {
|
|||||||
|
|
||||||
impl NuMenuCompleter {
|
impl NuMenuCompleter {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
block_id: usize,
|
block_id: BlockId,
|
||||||
span: Span,
|
span: Span,
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
engine_state: Arc<EngineState>,
|
engine_state: Arc<EngineState>,
|
||||||
@ -28,7 +28,7 @@ impl NuMenuCompleter {
|
|||||||
Self {
|
Self {
|
||||||
block_id,
|
block_id,
|
||||||
span,
|
span,
|
||||||
stack: stack.reset_out_dest().capture(),
|
stack: stack.reset_out_dest().collect_value(),
|
||||||
engine_state,
|
engine_state,
|
||||||
only_buffer_difference,
|
only_buffer_difference,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::ByteStreamSource;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Print;
|
pub struct Print;
|
||||||
@ -50,7 +51,7 @@ Since this command has no output, there is no point in piping it with other comm
|
|||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
mut input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let args: Vec<Value> = call.rest(engine_state, stack, 0)?;
|
let args: Vec<Value> = call.rest(engine_state, stack, 0)?;
|
||||||
let no_newline = call.has_flag(engine_state, stack, "no-newline")?;
|
let no_newline = call.has_flag(engine_state, stack, "no-newline")?;
|
||||||
@ -69,6 +70,11 @@ Since this command has no output, there is no point in piping it with other comm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !input.is_nothing() {
|
} else if !input.is_nothing() {
|
||||||
|
if let PipelineData::ByteStream(stream, _) = &mut input {
|
||||||
|
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
||||||
|
child.ignore_error(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
if raw {
|
if raw {
|
||||||
input.print_raw(engine_state, no_newline, to_stderr)?;
|
input.print_raw(engine_state, no_newline, to_stderr)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
use crate::prompt_update::{
|
use crate::prompt_update::{
|
||||||
POST_PROMPT_MARKER, PRE_PROMPT_MARKER, VSCODE_POST_PROMPT_MARKER, VSCODE_PRE_PROMPT_MARKER,
|
POST_PROMPT_MARKER, PRE_PROMPT_MARKER, VSCODE_POST_PROMPT_MARKER, VSCODE_PRE_PROMPT_MARKER,
|
||||||
};
|
};
|
||||||
use nu_protocol::{
|
use nu_protocol::engine::{EngineState, Stack};
|
||||||
engine::{EngineState, Stack},
|
|
||||||
Value,
|
|
||||||
};
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use nu_utils::enable_vt_processing;
|
use nu_utils::enable_vt_processing;
|
||||||
use reedline::{
|
use reedline::{
|
||||||
@ -124,8 +121,11 @@ impl Prompt for NushellPrompt {
|
|||||||
.replace('\n', "\r\n");
|
.replace('\n', "\r\n");
|
||||||
|
|
||||||
if self.shell_integration_osc633 {
|
if self.shell_integration_osc633 {
|
||||||
if self.stack.get_env_var(&self.engine_state, "TERM_PROGRAM")
|
if self
|
||||||
== Some(Value::test_string("vscode"))
|
.stack
|
||||||
|
.get_env_var(&self.engine_state, "TERM_PROGRAM")
|
||||||
|
.and_then(|v| v.as_str().ok())
|
||||||
|
== Some("vscode")
|
||||||
{
|
{
|
||||||
// We're in vscode and we have osc633 enabled
|
// We're in vscode and we have osc633 enabled
|
||||||
format!("{VSCODE_PRE_PROMPT_MARKER}{prompt}{VSCODE_POST_PROMPT_MARKER}").into()
|
format!("{VSCODE_PRE_PROMPT_MARKER}{prompt}{VSCODE_POST_PROMPT_MARKER}").into()
|
||||||
|
@ -68,7 +68,7 @@ fn get_prompt_string(
|
|||||||
.get_env_var(engine_state, prompt)
|
.get_env_var(engine_state, prompt)
|
||||||
.and_then(|v| match v {
|
.and_then(|v| match v {
|
||||||
Value::Closure { val, .. } => {
|
Value::Closure { val, .. } => {
|
||||||
let result = ClosureEvalOnce::new(engine_state, stack, *val)
|
let result = ClosureEvalOnce::new(engine_state, stack, val.as_ref().clone())
|
||||||
.run_with_input(PipelineData::Empty);
|
.run_with_input(PipelineData::Empty);
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
@ -119,7 +119,11 @@ pub(crate) fn update_prompt(
|
|||||||
// Now that we have the prompt string lets ansify it.
|
// Now that we have the prompt string lets ansify it.
|
||||||
// <133 A><prompt><133 B><command><133 C><command output>
|
// <133 A><prompt><133 B><command><133 C><command output>
|
||||||
let left_prompt_string = if config.shell_integration.osc633 {
|
let left_prompt_string = if config.shell_integration.osc633 {
|
||||||
if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) {
|
if stack
|
||||||
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
||||||
|
.and_then(|v| v.as_str().ok())
|
||||||
|
== Some("vscode")
|
||||||
|
{
|
||||||
// We're in vscode and we have osc633 enabled
|
// We're in vscode and we have osc633 enabled
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"{VSCODE_PRE_PROMPT_MARKER}{configured_left_prompt_string}{VSCODE_POST_PROMPT_MARKER}"
|
"{VSCODE_PRE_PROMPT_MARKER}{configured_left_prompt_string}{VSCODE_POST_PROMPT_MARKER}"
|
||||||
|
@ -5,11 +5,10 @@ use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
|||||||
use nu_engine::eval_block;
|
use nu_engine::eval_block;
|
||||||
use nu_parser::parse;
|
use nu_parser::parse;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
create_menus,
|
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
extract_value, Config, EditBindings, ParsedKeybinding, ParsedMenu, PipelineData, Record,
|
extract_value, Config, EditBindings, FromValue, ParsedKeybinding, ParsedMenu, PipelineData,
|
||||||
ShellError, Span, Value,
|
Record, ShellError, Span, Type, Value,
|
||||||
};
|
};
|
||||||
use reedline::{
|
use reedline::{
|
||||||
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
||||||
@ -36,6 +35,41 @@ const DEFAULT_COMPLETION_MENU: &str = r#"
|
|||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
|
const DEFAULT_IDE_COMPLETION_MENU: &str = r#"
|
||||||
|
{
|
||||||
|
name: ide_completion_menu
|
||||||
|
only_buffer_difference: false
|
||||||
|
marker: "| "
|
||||||
|
type: {
|
||||||
|
layout: ide
|
||||||
|
min_completion_width: 0,
|
||||||
|
max_completion_width: 50,
|
||||||
|
max_completion_height: 10, # will be limited by the available lines in the terminal
|
||||||
|
padding: 0,
|
||||||
|
border: true,
|
||||||
|
cursor_offset: 0,
|
||||||
|
description_mode: "prefer_right"
|
||||||
|
min_description_width: 0
|
||||||
|
max_description_width: 50
|
||||||
|
max_description_height: 10
|
||||||
|
description_offset: 1
|
||||||
|
# If true, the cursor pos will be corrected, so the suggestions match up with the typed text
|
||||||
|
#
|
||||||
|
# C:\> str
|
||||||
|
# str join
|
||||||
|
# str trim
|
||||||
|
# str split
|
||||||
|
correct_cursor_pos: false
|
||||||
|
}
|
||||||
|
style: {
|
||||||
|
text: green
|
||||||
|
selected_text: { attr: r }
|
||||||
|
description_text: yellow
|
||||||
|
match_text: { attr: u }
|
||||||
|
selected_match_text: { attr: ur }
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
const DEFAULT_HISTORY_MENU: &str = r#"
|
const DEFAULT_HISTORY_MENU: &str = r#"
|
||||||
{
|
{
|
||||||
name: history_menu
|
name: history_menu
|
||||||
@ -95,6 +129,7 @@ pub(crate) fn add_menus(
|
|||||||
// Checking if the default menus have been added from the config file
|
// Checking if the default menus have been added from the config file
|
||||||
let default_menus = [
|
let default_menus = [
|
||||||
("completion_menu", DEFAULT_COMPLETION_MENU),
|
("completion_menu", DEFAULT_COMPLETION_MENU),
|
||||||
|
("ide_completion_menu", DEFAULT_IDE_COMPLETION_MENU),
|
||||||
("history_menu", DEFAULT_HISTORY_MENU),
|
("history_menu", DEFAULT_HISTORY_MENU),
|
||||||
("help_menu", DEFAULT_HELP_MENU),
|
("help_menu", DEFAULT_HELP_MENU),
|
||||||
];
|
];
|
||||||
@ -122,7 +157,7 @@ pub(crate) fn add_menus(
|
|||||||
|
|
||||||
engine_state.merge_delta(delta)?;
|
engine_state.merge_delta(delta)?;
|
||||||
|
|
||||||
let mut temp_stack = Stack::new().capture();
|
let mut temp_stack = Stack::new().collect_value();
|
||||||
let input = PipelineData::Empty;
|
let input = PipelineData::Empty;
|
||||||
menu_eval_results.push(eval_block::<WithoutDebug>(
|
menu_eval_results.push(eval_block::<WithoutDebug>(
|
||||||
&engine_state,
|
&engine_state,
|
||||||
@ -137,15 +172,13 @@ pub(crate) fn add_menus(
|
|||||||
|
|
||||||
for res in menu_eval_results.into_iter() {
|
for res in menu_eval_results.into_iter() {
|
||||||
if let PipelineData::Value(value, None) = res {
|
if let PipelineData::Value(value, None) = res {
|
||||||
for menu in create_menus(&value)? {
|
line_editor = add_menu(
|
||||||
line_editor = add_menu(
|
line_editor,
|
||||||
line_editor,
|
&ParsedMenu::from_value(value)?,
|
||||||
&menu,
|
new_engine_state_ref.clone(),
|
||||||
new_engine_state_ref.clone(),
|
stack,
|
||||||
stack,
|
config.clone(),
|
||||||
config.clone(),
|
)?;
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,22 +201,22 @@ fn add_menu(
|
|||||||
"list" => add_list_menu(line_editor, menu, engine_state, stack, config),
|
"list" => add_list_menu(line_editor, menu, engine_state, stack, config),
|
||||||
"ide" => add_ide_menu(line_editor, menu, engine_state, stack, config),
|
"ide" => add_ide_menu(line_editor, menu, engine_state, stack, config),
|
||||||
"description" => add_description_menu(line_editor, menu, engine_state, stack, config),
|
"description" => add_description_menu(line_editor, menu, engine_state, stack, config),
|
||||||
_ => Err(ShellError::UnsupportedConfigValue {
|
str => Err(ShellError::InvalidValue {
|
||||||
expected: "columnar, list, ide or description".to_string(),
|
valid: "'columnar', 'list', 'ide', or 'description'".into(),
|
||||||
value: menu.r#type.to_abbreviated_string(&config),
|
actual: format!("'{str}'"),
|
||||||
span: menu.r#type.span(),
|
span,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ShellError::UnsupportedConfigValue {
|
Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "only record type".to_string(),
|
expected: Type::record(),
|
||||||
value: menu.r#type.to_abbreviated_string(&config),
|
actual: menu.r#type.get_type(),
|
||||||
span: menu.r#type.span(),
|
span,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_style(record: &Record, name: &str, span: Span) -> Option<Style> {
|
fn get_style(record: &Record, name: &'static str, span: Span) -> Option<Style> {
|
||||||
extract_value(name, record, span)
|
extract_value(name, record, span)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|text| match text {
|
.map(|text| match text {
|
||||||
@ -262,30 +295,23 @@ pub(crate) fn add_columnar_menu(
|
|||||||
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
||||||
columnar_menu = columnar_menu.with_only_buffer_difference(only_buffer_difference);
|
columnar_menu = columnar_menu.with_only_buffer_difference(only_buffer_difference);
|
||||||
|
|
||||||
let span = menu.source.span();
|
let completer = if let Some(closure) = &menu.source {
|
||||||
match &menu.source {
|
let menu_completer = NuMenuCompleter::new(
|
||||||
Value::Nothing { .. } => {
|
closure.block_id,
|
||||||
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(columnar_menu))))
|
|
||||||
}
|
|
||||||
Value::Closure { val, .. } => {
|
|
||||||
let menu_completer = NuMenuCompleter::new(
|
|
||||||
val.block_id,
|
|
||||||
span,
|
|
||||||
stack.captures_to_stack(val.captures.clone()),
|
|
||||||
engine_state,
|
|
||||||
only_buffer_difference,
|
|
||||||
);
|
|
||||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
||||||
menu: Box::new(columnar_menu),
|
|
||||||
completer: Box::new(menu_completer),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
_ => Err(ShellError::UnsupportedConfigValue {
|
|
||||||
expected: "block or omitted value".to_string(),
|
|
||||||
value: menu.source.to_abbreviated_string(config),
|
|
||||||
span,
|
span,
|
||||||
}),
|
stack.captures_to_stack(closure.captures.clone()),
|
||||||
}
|
engine_state,
|
||||||
|
only_buffer_difference,
|
||||||
|
);
|
||||||
|
ReedlineMenu::WithCompleter {
|
||||||
|
menu: Box::new(columnar_menu),
|
||||||
|
completer: Box::new(menu_completer),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ReedlineMenu::EngineCompleter(Box::new(columnar_menu))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(line_editor.with_menu(completer))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a search menu to the line editor
|
// Adds a search menu to the line editor
|
||||||
@ -318,30 +344,23 @@ pub(crate) fn add_list_menu(
|
|||||||
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
||||||
list_menu = list_menu.with_only_buffer_difference(only_buffer_difference);
|
list_menu = list_menu.with_only_buffer_difference(only_buffer_difference);
|
||||||
|
|
||||||
let span = menu.source.span();
|
let completer = if let Some(closure) = &menu.source {
|
||||||
match &menu.source {
|
let menu_completer = NuMenuCompleter::new(
|
||||||
Value::Nothing { .. } => {
|
closure.block_id,
|
||||||
Ok(line_editor.with_menu(ReedlineMenu::HistoryMenu(Box::new(list_menu))))
|
span,
|
||||||
|
stack.captures_to_stack(closure.captures.clone()),
|
||||||
|
engine_state,
|
||||||
|
only_buffer_difference,
|
||||||
|
);
|
||||||
|
ReedlineMenu::WithCompleter {
|
||||||
|
menu: Box::new(list_menu),
|
||||||
|
completer: Box::new(menu_completer),
|
||||||
}
|
}
|
||||||
Value::Closure { val, .. } => {
|
} else {
|
||||||
let menu_completer = NuMenuCompleter::new(
|
ReedlineMenu::HistoryMenu(Box::new(list_menu))
|
||||||
val.block_id,
|
};
|
||||||
span,
|
|
||||||
stack.captures_to_stack(val.captures.clone()),
|
Ok(line_editor.with_menu(completer))
|
||||||
engine_state,
|
|
||||||
only_buffer_difference,
|
|
||||||
);
|
|
||||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
||||||
menu: Box::new(list_menu),
|
|
||||||
completer: Box::new(menu_completer),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
_ => Err(ShellError::UnsupportedConfigValue {
|
|
||||||
expected: "block or omitted value".to_string(),
|
|
||||||
value: menu.source.to_abbreviated_string(&config),
|
|
||||||
span: menu.source.span(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds an IDE menu to the line editor
|
// Adds an IDE menu to the line editor
|
||||||
@ -416,9 +435,9 @@ pub(crate) fn add_ide_menu(
|
|||||||
vertical,
|
vertical,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
return Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "bool or record".to_string(),
|
expected: Type::custom("bool or record"),
|
||||||
value: border.to_abbreviated_string(&config),
|
actual: border.get_type(),
|
||||||
span: border.span(),
|
span: border.span(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -439,10 +458,10 @@ pub(crate) fn add_ide_menu(
|
|||||||
"left" => ide_menu.with_description_mode(DescriptionMode::Left),
|
"left" => ide_menu.with_description_mode(DescriptionMode::Left),
|
||||||
"right" => ide_menu.with_description_mode(DescriptionMode::Right),
|
"right" => ide_menu.with_description_mode(DescriptionMode::Right),
|
||||||
"prefer_right" => ide_menu.with_description_mode(DescriptionMode::PreferRight),
|
"prefer_right" => ide_menu.with_description_mode(DescriptionMode::PreferRight),
|
||||||
_ => {
|
str => {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
return Err(ShellError::InvalidValue {
|
||||||
expected: "\"left\", \"right\" or \"prefer_right\"".to_string(),
|
valid: "'left', 'right', or 'prefer_right'".into(),
|
||||||
value: description_mode.to_abbreviated_string(&config),
|
actual: format!("'{str}'"),
|
||||||
span: description_mode.span(),
|
span: description_mode.span(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -499,30 +518,23 @@ pub(crate) fn add_ide_menu(
|
|||||||
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
||||||
ide_menu = ide_menu.with_only_buffer_difference(only_buffer_difference);
|
ide_menu = ide_menu.with_only_buffer_difference(only_buffer_difference);
|
||||||
|
|
||||||
let span = menu.source.span();
|
let completer = if let Some(closure) = &menu.source {
|
||||||
match &menu.source {
|
let menu_completer = NuMenuCompleter::new(
|
||||||
Value::Nothing { .. } => {
|
closure.block_id,
|
||||||
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(ide_menu))))
|
|
||||||
}
|
|
||||||
Value::Closure { val, .. } => {
|
|
||||||
let menu_completer = NuMenuCompleter::new(
|
|
||||||
val.block_id,
|
|
||||||
span,
|
|
||||||
stack.captures_to_stack(val.captures.clone()),
|
|
||||||
engine_state,
|
|
||||||
only_buffer_difference,
|
|
||||||
);
|
|
||||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
||||||
menu: Box::new(ide_menu),
|
|
||||||
completer: Box::new(menu_completer),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
_ => Err(ShellError::UnsupportedConfigValue {
|
|
||||||
expected: "block or omitted value".to_string(),
|
|
||||||
value: menu.source.to_abbreviated_string(&config),
|
|
||||||
span,
|
span,
|
||||||
}),
|
stack.captures_to_stack(closure.captures.clone()),
|
||||||
}
|
engine_state,
|
||||||
|
only_buffer_difference,
|
||||||
|
);
|
||||||
|
ReedlineMenu::WithCompleter {
|
||||||
|
menu: Box::new(ide_menu),
|
||||||
|
completer: Box::new(menu_completer),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ReedlineMenu::EngineCompleter(Box::new(ide_menu))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(line_editor.with_menu(completer))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a description menu to the line editor
|
// Adds a description menu to the line editor
|
||||||
@ -587,34 +599,27 @@ pub(crate) fn add_description_menu(
|
|||||||
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
||||||
description_menu = description_menu.with_only_buffer_difference(only_buffer_difference);
|
description_menu = description_menu.with_only_buffer_difference(only_buffer_difference);
|
||||||
|
|
||||||
let span = menu.source.span();
|
let completer = if let Some(closure) = &menu.source {
|
||||||
match &menu.source {
|
let menu_completer = NuMenuCompleter::new(
|
||||||
Value::Nothing { .. } => {
|
closure.block_id,
|
||||||
let completer = Box::new(NuHelpCompleter::new(engine_state, config));
|
span,
|
||||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
stack.captures_to_stack(closure.captures.clone()),
|
||||||
menu: Box::new(description_menu),
|
engine_state,
|
||||||
completer,
|
only_buffer_difference,
|
||||||
}))
|
);
|
||||||
|
ReedlineMenu::WithCompleter {
|
||||||
|
menu: Box::new(description_menu),
|
||||||
|
completer: Box::new(menu_completer),
|
||||||
}
|
}
|
||||||
Value::Closure { val, .. } => {
|
} else {
|
||||||
let menu_completer = NuMenuCompleter::new(
|
let menu_completer = NuHelpCompleter::new(engine_state, config);
|
||||||
val.block_id,
|
ReedlineMenu::WithCompleter {
|
||||||
span,
|
menu: Box::new(description_menu),
|
||||||
stack.captures_to_stack(val.captures.clone()),
|
completer: Box::new(menu_completer),
|
||||||
engine_state,
|
|
||||||
only_buffer_difference,
|
|
||||||
);
|
|
||||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
||||||
menu: Box::new(description_menu),
|
|
||||||
completer: Box::new(menu_completer),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
_ => Err(ShellError::UnsupportedConfigValue {
|
};
|
||||||
expected: "closure or omitted value".to_string(),
|
|
||||||
value: menu.source.to_abbreviated_string(&config),
|
Ok(line_editor.with_menu(completer))
|
||||||
span: menu.source.span(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
||||||
@ -629,6 +634,16 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
keybindings.add_binding(
|
||||||
|
KeyModifiers::CONTROL,
|
||||||
|
KeyCode::Char(' '),
|
||||||
|
ReedlineEvent::UntilFound(vec![
|
||||||
|
ReedlineEvent::Menu("ide_completion_menu".to_string()),
|
||||||
|
ReedlineEvent::MenuNext,
|
||||||
|
ReedlineEvent::Edit(vec![EditCommand::Complete]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
keybindings.add_binding(
|
keybindings.add_binding(
|
||||||
KeyModifiers::SHIFT,
|
KeyModifiers::SHIFT,
|
||||||
KeyCode::BackTab,
|
KeyCode::BackTab,
|
||||||
@ -696,6 +711,7 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
|
|||||||
}
|
}
|
||||||
for keybinding in parsed_keybindings {
|
for keybinding in parsed_keybindings {
|
||||||
add_keybinding(
|
add_keybinding(
|
||||||
|
&keybinding.name,
|
||||||
&keybinding.mode,
|
&keybinding.mode,
|
||||||
keybinding,
|
keybinding,
|
||||||
config,
|
config,
|
||||||
@ -714,7 +730,9 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::only_used_in_recursion)]
|
||||||
fn add_keybinding(
|
fn add_keybinding(
|
||||||
|
name: &Option<Value>,
|
||||||
mode: &Value,
|
mode: &Value,
|
||||||
keybinding: &ParsedKeybinding,
|
keybinding: &ParsedKeybinding,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
@ -728,15 +746,16 @@ fn add_keybinding(
|
|||||||
"emacs" => add_parsed_keybinding(emacs_keybindings, keybinding, config),
|
"emacs" => add_parsed_keybinding(emacs_keybindings, keybinding, config),
|
||||||
"vi_insert" => add_parsed_keybinding(insert_keybindings, keybinding, config),
|
"vi_insert" => add_parsed_keybinding(insert_keybindings, keybinding, config),
|
||||||
"vi_normal" => add_parsed_keybinding(normal_keybindings, keybinding, config),
|
"vi_normal" => add_parsed_keybinding(normal_keybindings, keybinding, config),
|
||||||
m => Err(ShellError::UnsupportedConfigValue {
|
str => Err(ShellError::InvalidValue {
|
||||||
expected: "emacs, vi_insert or vi_normal".to_string(),
|
valid: "'emacs', 'vi_insert', or 'vi_normal'".into(),
|
||||||
value: m.to_string(),
|
actual: format!("'{str}'"),
|
||||||
span,
|
span,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
for inner_mode in vals {
|
for inner_mode in vals {
|
||||||
add_keybinding(
|
add_keybinding(
|
||||||
|
name,
|
||||||
inner_mode,
|
inner_mode,
|
||||||
keybinding,
|
keybinding,
|
||||||
config,
|
config,
|
||||||
@ -748,9 +767,9 @@ fn add_keybinding(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
v => Err(ShellError::UnsupportedConfigValue {
|
v => Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "string or list of strings".to_string(),
|
expected: Type::custom("string or list<string>"),
|
||||||
value: v.to_abbreviated_string(config),
|
actual: v.get_type(),
|
||||||
span: v.span(),
|
span: v.span(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@ -761,91 +780,107 @@ fn add_parsed_keybinding(
|
|||||||
keybinding: &ParsedKeybinding,
|
keybinding: &ParsedKeybinding,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> Result<(), ShellError> {
|
) -> Result<(), ShellError> {
|
||||||
let modifier = match keybinding
|
let Ok(modifier_str) = keybinding.modifier.as_str() else {
|
||||||
.modifier
|
return Err(ShellError::RuntimeTypeMismatch {
|
||||||
.to_expanded_string("", config)
|
expected: Type::String,
|
||||||
.to_ascii_lowercase()
|
actual: keybinding.modifier.get_type(),
|
||||||
.as_str()
|
span: keybinding.modifier.span(),
|
||||||
{
|
});
|
||||||
"control" => KeyModifiers::CONTROL,
|
|
||||||
"shift" => KeyModifiers::SHIFT,
|
|
||||||
"alt" => KeyModifiers::ALT,
|
|
||||||
"none" => KeyModifiers::NONE,
|
|
||||||
"shift_alt" | "alt_shift" => KeyModifiers::SHIFT | KeyModifiers::ALT,
|
|
||||||
"control_shift" | "shift_control" => KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
|
||||||
"control_alt" | "alt_control" => KeyModifiers::CONTROL | KeyModifiers::ALT,
|
|
||||||
"control_alt_shift" | "control_shift_alt" => {
|
|
||||||
KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
|
||||||
expected: "CONTROL, SHIFT, ALT or NONE".to_string(),
|
|
||||||
value: keybinding.modifier.to_abbreviated_string(config),
|
|
||||||
span: keybinding.modifier.span(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let keycode = match keybinding
|
let mut modifier = KeyModifiers::NONE;
|
||||||
.keycode
|
if !str::eq_ignore_ascii_case(modifier_str, "none") {
|
||||||
.to_expanded_string("", config)
|
for part in modifier_str.split('_') {
|
||||||
.to_ascii_lowercase()
|
match part.to_ascii_lowercase().as_str() {
|
||||||
.as_str()
|
"control" => modifier |= KeyModifiers::CONTROL,
|
||||||
{
|
"shift" => modifier |= KeyModifiers::SHIFT,
|
||||||
"backspace" => KeyCode::Backspace,
|
"alt" => modifier |= KeyModifiers::ALT,
|
||||||
"enter" => KeyCode::Enter,
|
"super" => modifier |= KeyModifiers::SUPER,
|
||||||
c if c.starts_with("char_") => {
|
"hyper" => modifier |= KeyModifiers::HYPER,
|
||||||
let mut char_iter = c.chars().skip(5);
|
"meta" => modifier |= KeyModifiers::META,
|
||||||
let pos1 = char_iter.next();
|
_ => {
|
||||||
let pos2 = char_iter.next();
|
return Err(ShellError::InvalidValue {
|
||||||
|
valid: "'control', 'shift', 'alt', 'super', 'hyper', 'meta', or 'none'"
|
||||||
let char = if let (Some(char), None) = (pos1, pos2) {
|
.into(),
|
||||||
char
|
actual: format!("'{part}'"),
|
||||||
} else {
|
span: keybinding.modifier.span(),
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
});
|
||||||
expected: "char_<CHAR: unicode codepoint>".to_string(),
|
}
|
||||||
value: c.to_string(),
|
}
|
||||||
span: keybinding.keycode.span(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
KeyCode::Char(char)
|
|
||||||
}
|
}
|
||||||
"space" => KeyCode::Char(' '),
|
}
|
||||||
"down" => KeyCode::Down,
|
|
||||||
"up" => KeyCode::Up,
|
let Ok(keycode) = keybinding.keycode.as_str() else {
|
||||||
"left" => KeyCode::Left,
|
return Err(ShellError::RuntimeTypeMismatch {
|
||||||
"right" => KeyCode::Right,
|
expected: Type::String,
|
||||||
"home" => KeyCode::Home,
|
actual: keybinding.keycode.get_type(),
|
||||||
"end" => KeyCode::End,
|
span: keybinding.keycode.span(),
|
||||||
"pageup" => KeyCode::PageUp,
|
});
|
||||||
"pagedown" => KeyCode::PageDown,
|
};
|
||||||
"tab" => KeyCode::Tab,
|
|
||||||
"backtab" => KeyCode::BackTab,
|
let keycode_lower = keycode.to_ascii_lowercase();
|
||||||
"delete" => KeyCode::Delete,
|
|
||||||
"insert" => KeyCode::Insert,
|
let keycode = if let Some(rest) = keycode_lower.strip_prefix("char_") {
|
||||||
c if c.starts_with('f') => {
|
let error = |valid: &str, actual: &str| ShellError::InvalidValue {
|
||||||
let fn_num: u8 = c[1..]
|
valid: valid.into(),
|
||||||
|
actual: actual.into(),
|
||||||
|
span: keybinding.keycode.span(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut char_iter = rest.chars();
|
||||||
|
let char = match (char_iter.next(), char_iter.next()) {
|
||||||
|
(Some(char), None) => char,
|
||||||
|
(Some('u'), Some(_)) => {
|
||||||
|
// This will never panic as we know there are at least two symbols
|
||||||
|
let Ok(code_point) = u32::from_str_radix(&rest[1..], 16) else {
|
||||||
|
return Err(error("a valid hex code", keycode));
|
||||||
|
};
|
||||||
|
|
||||||
|
char::from_u32(code_point).ok_or(error("a valid Unicode code point", keycode))?
|
||||||
|
}
|
||||||
|
_ => return Err(error("'char_<char>' or 'char_u<hex code>'", keycode)),
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyCode::Char(char)
|
||||||
|
} else {
|
||||||
|
match keycode_lower.as_str() {
|
||||||
|
"backspace" => KeyCode::Backspace,
|
||||||
|
"enter" => KeyCode::Enter,
|
||||||
|
"space" => KeyCode::Char(' '),
|
||||||
|
"down" => KeyCode::Down,
|
||||||
|
"up" => KeyCode::Up,
|
||||||
|
"left" => KeyCode::Left,
|
||||||
|
"right" => KeyCode::Right,
|
||||||
|
"home" => KeyCode::Home,
|
||||||
|
"end" => KeyCode::End,
|
||||||
|
"pageup" => KeyCode::PageUp,
|
||||||
|
"pagedown" => KeyCode::PageDown,
|
||||||
|
"tab" => KeyCode::Tab,
|
||||||
|
"backtab" => KeyCode::BackTab,
|
||||||
|
"delete" => KeyCode::Delete,
|
||||||
|
"insert" => KeyCode::Insert,
|
||||||
|
c if c.starts_with('f') => c[1..]
|
||||||
.parse()
|
.parse()
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|num| matches!(num, 1..=20))
|
.filter(|num| (1..=35).contains(num))
|
||||||
.ok_or(ShellError::UnsupportedConfigValue {
|
.map(KeyCode::F)
|
||||||
expected: "(f1|f2|...|f20)".to_string(),
|
.ok_or(ShellError::InvalidValue {
|
||||||
value: format!("unknown function key: {c}"),
|
valid: "'f1', 'f2', ..., or 'f35'".into(),
|
||||||
|
actual: format!("'{keycode}'"),
|
||||||
span: keybinding.keycode.span(),
|
span: keybinding.keycode.span(),
|
||||||
})?;
|
})?,
|
||||||
KeyCode::F(fn_num)
|
"null" => KeyCode::Null,
|
||||||
}
|
"esc" | "escape" => KeyCode::Esc,
|
||||||
"null" => KeyCode::Null,
|
_ => {
|
||||||
"esc" | "escape" => KeyCode::Esc,
|
return Err(ShellError::InvalidValue {
|
||||||
_ => {
|
valid: "a crossterm KeyCode".into(),
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
actual: format!("'{keycode}'"),
|
||||||
expected: "crossterm KeyCode".to_string(),
|
span: keybinding.keycode.span(),
|
||||||
value: keybinding.keycode.to_abbreviated_string(config),
|
});
|
||||||
span: keybinding.keycode.span(),
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(event) = parse_event(&keybinding.event, config)? {
|
if let Some(event) = parse_event(&keybinding.event, config)? {
|
||||||
keybindings.add_binding(modifier, keycode, event);
|
keybindings.add_binding(modifier, keycode, event);
|
||||||
} else {
|
} else {
|
||||||
@ -867,8 +902,8 @@ impl<'config> EventType<'config> {
|
|||||||
.map(Self::Send)
|
.map(Self::Send)
|
||||||
.or_else(|_| extract_value("edit", record, span).map(Self::Edit))
|
.or_else(|_| extract_value("edit", record, span).map(Self::Edit))
|
||||||
.or_else(|_| extract_value("until", record, span).map(Self::Until))
|
.or_else(|_| extract_value("until", record, span).map(Self::Until))
|
||||||
.map_err(|_| ShellError::MissingConfigValue {
|
.map_err(|_| ShellError::MissingRequiredColumn {
|
||||||
missing_value: "send, edit or until".to_string(),
|
column: "'send', 'edit', or 'until'",
|
||||||
span,
|
span,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -906,9 +941,9 @@ fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>,
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|value| match parse_event(value, config) {
|
.map(|value| match parse_event(value, config) {
|
||||||
Ok(inner) => match inner {
|
Ok(inner) => match inner {
|
||||||
None => Err(ShellError::UnsupportedConfigValue {
|
None => Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "List containing valid events".to_string(),
|
expected: Type::custom("record or table"),
|
||||||
value: "Nothing value (null)".to_string(),
|
actual: value.get_type(),
|
||||||
span: value.span(),
|
span: value.span(),
|
||||||
}),
|
}),
|
||||||
Some(event) => Ok(event),
|
Some(event) => Ok(event),
|
||||||
@ -919,9 +954,9 @@ fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>,
|
|||||||
|
|
||||||
Ok(Some(ReedlineEvent::UntilFound(events)))
|
Ok(Some(ReedlineEvent::UntilFound(events)))
|
||||||
}
|
}
|
||||||
v => Err(ShellError::UnsupportedConfigValue {
|
v => Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "list of events".to_string(),
|
expected: Type::list(Type::Any),
|
||||||
value: v.to_abbreviated_string(config),
|
actual: v.get_type(),
|
||||||
span: v.span(),
|
span: v.span(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -931,9 +966,9 @@ fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>,
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|value| match parse_event(value, config) {
|
.map(|value| match parse_event(value, config) {
|
||||||
Ok(inner) => match inner {
|
Ok(inner) => match inner {
|
||||||
None => Err(ShellError::UnsupportedConfigValue {
|
None => Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "List containing valid events".to_string(),
|
expected: Type::custom("record or table"),
|
||||||
value: "Nothing value (null)".to_string(),
|
actual: value.get_type(),
|
||||||
span: value.span(),
|
span: value.span(),
|
||||||
}),
|
}),
|
||||||
Some(event) => Ok(event),
|
Some(event) => Ok(event),
|
||||||
@ -945,9 +980,9 @@ fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>,
|
|||||||
Ok(Some(ReedlineEvent::Multiple(events)))
|
Ok(Some(ReedlineEvent::Multiple(events)))
|
||||||
}
|
}
|
||||||
Value::Nothing { .. } => Ok(None),
|
Value::Nothing { .. } => Ok(None),
|
||||||
v => Err(ShellError::UnsupportedConfigValue {
|
v => Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "record or list of records, null to unbind key".to_string(),
|
expected: Type::custom("record, table, or nothing"),
|
||||||
value: v.to_abbreviated_string(config),
|
actual: v.get_type(),
|
||||||
span: v.span(),
|
span: v.span(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@ -996,12 +1031,12 @@ fn event_from_record(
|
|||||||
let cmd = extract_value("cmd", record, span)?;
|
let cmd = extract_value("cmd", record, span)?;
|
||||||
ReedlineEvent::ExecuteHostCommand(cmd.to_expanded_string("", config))
|
ReedlineEvent::ExecuteHostCommand(cmd.to_expanded_string("", config))
|
||||||
}
|
}
|
||||||
v => {
|
str => {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
return Err(ShellError::InvalidValue {
|
||||||
expected: "Reedline event".to_string(),
|
valid: "a reedline event".into(),
|
||||||
value: v.to_string(),
|
actual: format!("'{str}'"),
|
||||||
span,
|
span,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1094,7 +1129,7 @@ fn edit_from_record(
|
|||||||
}
|
}
|
||||||
"insertchar" => {
|
"insertchar" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
EditCommand::InsertChar(char)
|
EditCommand::InsertChar(char)
|
||||||
}
|
}
|
||||||
"insertstring" => {
|
"insertstring" => {
|
||||||
@ -1131,17 +1166,17 @@ fn edit_from_record(
|
|||||||
"redo" => EditCommand::Redo,
|
"redo" => EditCommand::Redo,
|
||||||
"cutrightuntil" => {
|
"cutrightuntil" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
EditCommand::CutRightUntil(char)
|
EditCommand::CutRightUntil(char)
|
||||||
}
|
}
|
||||||
"cutrightbefore" => {
|
"cutrightbefore" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
EditCommand::CutRightBefore(char)
|
EditCommand::CutRightBefore(char)
|
||||||
}
|
}
|
||||||
"moverightuntil" => {
|
"moverightuntil" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
let select = extract_value("select", record, span)
|
let select = extract_value("select", record, span)
|
||||||
.and_then(|value| value.as_bool())
|
.and_then(|value| value.as_bool())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@ -1149,7 +1184,7 @@ fn edit_from_record(
|
|||||||
}
|
}
|
||||||
"moverightbefore" => {
|
"moverightbefore" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
let select = extract_value("select", record, span)
|
let select = extract_value("select", record, span)
|
||||||
.and_then(|value| value.as_bool())
|
.and_then(|value| value.as_bool())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@ -1157,17 +1192,17 @@ fn edit_from_record(
|
|||||||
}
|
}
|
||||||
"cutleftuntil" => {
|
"cutleftuntil" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
EditCommand::CutLeftUntil(char)
|
EditCommand::CutLeftUntil(char)
|
||||||
}
|
}
|
||||||
"cutleftbefore" => {
|
"cutleftbefore" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
EditCommand::CutLeftBefore(char)
|
EditCommand::CutLeftBefore(char)
|
||||||
}
|
}
|
||||||
"moveleftuntil" => {
|
"moveleftuntil" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
let select = extract_value("select", record, span)
|
let select = extract_value("select", record, span)
|
||||||
.and_then(|value| value.as_bool())
|
.and_then(|value| value.as_bool())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@ -1175,7 +1210,7 @@ fn edit_from_record(
|
|||||||
}
|
}
|
||||||
"moveleftbefore" => {
|
"moveleftbefore" => {
|
||||||
let value = extract_value("value", record, span)?;
|
let value = extract_value("value", record, span)?;
|
||||||
let char = extract_char(value, config)?;
|
let char = extract_char(value)?;
|
||||||
let select = extract_value("select", record, span)
|
let select = extract_value("select", record, span)
|
||||||
.and_then(|value| value.as_bool())
|
.and_then(|value| value.as_bool())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@ -1192,28 +1227,36 @@ fn edit_from_record(
|
|||||||
#[cfg(feature = "system-clipboard")]
|
#[cfg(feature = "system-clipboard")]
|
||||||
"pastesystem" => EditCommand::PasteSystem,
|
"pastesystem" => EditCommand::PasteSystem,
|
||||||
"selectall" => EditCommand::SelectAll,
|
"selectall" => EditCommand::SelectAll,
|
||||||
e => {
|
str => {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
return Err(ShellError::InvalidValue {
|
||||||
expected: "reedline EditCommand".to_string(),
|
valid: "a reedline EditCommand".into(),
|
||||||
value: e.to_string(),
|
actual: format!("'{str}'"),
|
||||||
span,
|
span,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(edit)
|
Ok(edit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_char(value: &Value, config: &Config) -> Result<char, ShellError> {
|
fn extract_char(value: &Value) -> Result<char, ShellError> {
|
||||||
let span = value.span();
|
if let Ok(str) = value.as_str() {
|
||||||
value
|
let mut chars = str.chars();
|
||||||
.to_expanded_string("", config)
|
match (chars.next(), chars.next()) {
|
||||||
.chars()
|
(Some(c), None) => Ok(c),
|
||||||
.next()
|
_ => Err(ShellError::InvalidValue {
|
||||||
.ok_or_else(|| ShellError::MissingConfigValue {
|
valid: "a single character".into(),
|
||||||
missing_value: "char to insert".to_string(),
|
actual: format!("'{str}'"),
|
||||||
span,
|
span: value.span(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(ShellError::RuntimeTypeMismatch {
|
||||||
|
expected: Type::String,
|
||||||
|
actual: value.get_type(),
|
||||||
|
span: value.span(),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -1342,7 +1385,7 @@ mod test {
|
|||||||
|
|
||||||
let span = Span::test_data();
|
let span = Span::test_data();
|
||||||
let b = EventType::try_from_record(&event, span);
|
let b = EventType::try_from_record(&event, span);
|
||||||
assert!(matches!(b, Err(ShellError::MissingConfigValue { .. })));
|
assert!(matches!(b, Err(ShellError::MissingRequiredColumn { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -16,10 +16,7 @@ use crate::{
|
|||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::SetCursorStyle;
|
||||||
use log::{error, trace, warn};
|
use log::{error, trace, warn};
|
||||||
use miette::{ErrReport, IntoDiagnostic, Result};
|
use miette::{ErrReport, IntoDiagnostic, Result};
|
||||||
use nu_cmd_base::{
|
use nu_cmd_base::{hook::eval_hook, util::get_editor};
|
||||||
hook::eval_hook,
|
|
||||||
util::{get_editor, get_guaranteed_cwd},
|
|
||||||
};
|
|
||||||
use nu_color_config::StyleComputer;
|
use nu_color_config::StyleComputer;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use nu_engine::{convert_env_values, current_dir_str, env_to_strings};
|
use nu_engine::{convert_env_values, current_dir_str, env_to_strings};
|
||||||
@ -53,7 +50,6 @@ use sysinfo::System;
|
|||||||
pub fn evaluate_repl(
|
pub fn evaluate_repl(
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
nushell_path: &str,
|
|
||||||
prerun_command: Option<Spanned<String>>,
|
prerun_command: Option<Spanned<String>>,
|
||||||
load_std_lib: Option<Spanned<String>>,
|
load_std_lib: Option<Spanned<String>>,
|
||||||
entire_start_time: Instant,
|
entire_start_time: Instant,
|
||||||
@ -100,7 +96,7 @@ pub fn evaluate_repl(
|
|||||||
|
|
||||||
unique_stack.set_last_exit_code(0, Span::unknown());
|
unique_stack.set_last_exit_code(0, Span::unknown());
|
||||||
|
|
||||||
let mut line_editor = get_line_editor(engine_state, nushell_path, use_color)?;
|
let mut line_editor = get_line_editor(engine_state, use_color)?;
|
||||||
let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
|
let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
|
||||||
|
|
||||||
if let Some(s) = prerun_command {
|
if let Some(s) = prerun_command {
|
||||||
@ -112,8 +108,7 @@ pub fn evaluate_repl(
|
|||||||
PipelineData::empty(),
|
PipelineData::empty(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
let cwd = get_guaranteed_cwd(engine_state, &unique_stack);
|
engine_state.merge_env(&mut unique_stack)?;
|
||||||
engine_state.merge_env(&mut unique_stack, cwd)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let hostname = System::host_name();
|
let hostname = System::host_name();
|
||||||
@ -136,15 +131,7 @@ pub fn evaluate_repl(
|
|||||||
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
||||||
let cmd_text = line_editor.current_buffer_contents().to_string();
|
let cmd_text = line_editor.current_buffer_contents().to_string();
|
||||||
|
|
||||||
let replaced_cmd_text = cmd_text
|
let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?;
|
||||||
.chars()
|
|
||||||
.map(|c| match c {
|
|
||||||
'\n' => '\x0a',
|
|
||||||
'\r' => '\x0d',
|
|
||||||
'\x1b' => '\x1b',
|
|
||||||
_ => c,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
run_shell_integration_osc633(
|
run_shell_integration_osc633(
|
||||||
engine_state,
|
engine_state,
|
||||||
@ -163,7 +150,7 @@ pub fn evaluate_repl(
|
|||||||
eval_source(
|
eval_source(
|
||||||
engine_state,
|
engine_state,
|
||||||
&mut unique_stack,
|
&mut unique_stack,
|
||||||
r#"use std banner; banner"#.as_bytes(),
|
r#"banner"#.as_bytes(),
|
||||||
"show_banner",
|
"show_banner",
|
||||||
PipelineData::empty(),
|
PipelineData::empty(),
|
||||||
false,
|
false,
|
||||||
@ -220,7 +207,7 @@ pub fn evaluate_repl(
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// line_editor is lost in the error case so reconstruct a new one
|
// line_editor is lost in the error case so reconstruct a new one
|
||||||
line_editor = get_line_editor(engine_state, nushell_path, use_color)?;
|
line_editor = get_line_editor(engine_state, use_color)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,11 +215,44 @@ pub fn evaluate_repl(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_line_editor(
|
fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
|
||||||
engine_state: &mut EngineState,
|
let bytes = input
|
||||||
nushell_path: &str,
|
.chars()
|
||||||
use_color: bool,
|
.flat_map(|c| {
|
||||||
) -> Result<Reedline> {
|
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> {
|
||||||
let mut start_time = std::time::Instant::now();
|
let mut start_time = std::time::Instant::now();
|
||||||
let mut line_editor = Reedline::create();
|
let mut line_editor = Reedline::create();
|
||||||
|
|
||||||
@ -243,7 +263,7 @@ fn get_line_editor(
|
|||||||
if let Some(history) = engine_state.history_config() {
|
if let Some(history) = engine_state.history_config() {
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
|
|
||||||
line_editor = setup_history(nushell_path, engine_state, line_editor, history)?;
|
line_editor = setup_history(engine_state, line_editor, history)?;
|
||||||
|
|
||||||
perf!("setup history", start_time, use_color);
|
perf!("setup history", start_time, use_color);
|
||||||
}
|
}
|
||||||
@ -280,12 +300,10 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
|||||||
hostname,
|
hostname,
|
||||||
} = ctx;
|
} = ctx;
|
||||||
|
|
||||||
let cwd = get_guaranteed_cwd(engine_state, &stack);
|
|
||||||
|
|
||||||
let mut start_time = std::time::Instant::now();
|
let mut start_time = std::time::Instant::now();
|
||||||
// Before doing anything, merge the environment from the previous REPL iteration into the
|
// Before doing anything, merge the environment from the previous REPL iteration into the
|
||||||
// permanent state.
|
// permanent state.
|
||||||
if let Err(err) = engine_state.merge_env(&mut stack, cwd) {
|
if let Err(err) = engine_state.merge_env(&mut stack) {
|
||||||
report_shell_error(engine_state, &err);
|
report_shell_error(engine_state, &err);
|
||||||
}
|
}
|
||||||
// Check whether $env.NU_DISABLE_IR is set, so that the user can change it in the REPL
|
// Check whether $env.NU_DISABLE_IR is set, so that the user can change it in the REPL
|
||||||
@ -363,7 +381,11 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
|||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
))
|
))
|
||||||
.with_cursor_config(cursor_config);
|
.with_cursor_config(cursor_config)
|
||||||
|
.with_visual_selection_style(nu_ansi_term::Style {
|
||||||
|
is_reverse: true,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
perf!("reedline builder", start_time, use_color);
|
perf!("reedline builder", start_time, use_color);
|
||||||
|
|
||||||
@ -518,8 +540,10 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
|||||||
drop(repl);
|
drop(repl);
|
||||||
|
|
||||||
if shell_integration_osc633 {
|
if shell_integration_osc633 {
|
||||||
if stack.get_env_var(engine_state, "TERM_PROGRAM")
|
if stack
|
||||||
== Some(Value::test_string("vscode"))
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
||||||
|
.and_then(|v| v.as_str().ok())
|
||||||
|
== Some("vscode")
|
||||||
{
|
{
|
||||||
start_time = Instant::now();
|
start_time = Instant::now();
|
||||||
|
|
||||||
@ -736,7 +760,7 @@ fn fill_in_result_related_history_metadata(
|
|||||||
c.duration = Some(cmd_duration);
|
c.duration = Some(cmd_duration);
|
||||||
c.exit_status = stack
|
c.exit_status = stack
|
||||||
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
||||||
.and_then(|e| e.as_i64().ok());
|
.and_then(|e| e.as_int().ok());
|
||||||
c
|
c
|
||||||
})
|
})
|
||||||
.into_diagnostic()?; // todo: don't stop repl if error here?
|
.into_diagnostic()?; // todo: don't stop repl if error here?
|
||||||
@ -841,7 +865,7 @@ fn do_auto_cd(
|
|||||||
|
|
||||||
let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
|
let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
|
||||||
let mut shells = if let Some(v) = shells {
|
let mut shells = if let Some(v) = shells {
|
||||||
v.into_list().unwrap_or_else(|_| vec![cwd])
|
v.clone().into_list().unwrap_or_else(|_| vec![cwd])
|
||||||
} else {
|
} else {
|
||||||
vec![cwd]
|
vec![cwd]
|
||||||
};
|
};
|
||||||
@ -1033,7 +1057,11 @@ fn run_shell_integration_osc633(
|
|||||||
if let Ok(path) = current_dir_str(engine_state, stack) {
|
if let Ok(path) = current_dir_str(engine_state, stack) {
|
||||||
// Supported escape sequences of Microsoft's Visual Studio Code (vscode)
|
// Supported escape sequences of Microsoft's Visual Studio Code (vscode)
|
||||||
// https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
|
// https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
|
||||||
if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) {
|
if stack
|
||||||
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
||||||
|
.and_then(|v| v.as_str().ok())
|
||||||
|
== Some("vscode")
|
||||||
|
{
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
// If we're in vscode, run their specific ansi escape sequence.
|
// If we're in vscode, run their specific ansi escape sequence.
|
||||||
@ -1051,16 +1079,8 @@ fn run_shell_integration_osc633(
|
|||||||
|
|
||||||
// escape a few things because this says so
|
// escape a few things because this says so
|
||||||
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
||||||
|
let replaced_cmd_text =
|
||||||
let replaced_cmd_text: String = repl_cmd_line_text
|
escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
|
||||||
.chars()
|
|
||||||
.map(|c| match c {
|
|
||||||
'\n' => '\x0a',
|
|
||||||
'\r' => '\x0d',
|
|
||||||
'\x1b' => '\x1b',
|
|
||||||
_ => c,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
//OSC 633 ; E ; <commandline> [; <nonce] ST - Explicitly set the command line with an optional nonce.
|
//OSC 633 ; E ; <commandline> [; <nonce] ST - Explicitly set the command line with an optional nonce.
|
||||||
run_ansi_sequence(&format!(
|
run_ansi_sequence(&format!(
|
||||||
@ -1098,7 +1118,6 @@ fn flush_engine_state_repl_buffer(engine_state: &mut EngineState, line_editor: &
|
|||||||
/// Setup history management for Reedline
|
/// Setup history management for Reedline
|
||||||
///
|
///
|
||||||
fn setup_history(
|
fn setup_history(
|
||||||
nushell_path: &str,
|
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
line_editor: Reedline,
|
line_editor: Reedline,
|
||||||
history: HistoryConfig,
|
history: HistoryConfig,
|
||||||
@ -1110,7 +1129,7 @@ fn setup_history(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(path) = crate::config_files::get_history_path(nushell_path, history.file_format) {
|
if let Some(path) = history.file_path() {
|
||||||
return update_line_editor_history(
|
return update_line_editor_history(
|
||||||
engine_state,
|
engine_state,
|
||||||
path,
|
path,
|
||||||
@ -1228,10 +1247,14 @@ fn get_command_finished_marker(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let exit_code = stack
|
let exit_code = stack
|
||||||
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
||||||
.and_then(|e| e.as_i64().ok());
|
.and_then(|e| e.as_int().ok());
|
||||||
|
|
||||||
if shell_integration_osc633 {
|
if shell_integration_osc633 {
|
||||||
if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) {
|
if stack
|
||||||
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
||||||
|
.and_then(|v| v.as_str().ok())
|
||||||
|
== Some("vscode")
|
||||||
|
{
|
||||||
// We're in vscode and we have osc633 enabled
|
// We're in vscode and we have osc633 enabled
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
@ -1280,7 +1303,11 @@ fn run_finaliziation_ansi_sequence(
|
|||||||
) {
|
) {
|
||||||
if shell_integration_osc633 {
|
if shell_integration_osc633 {
|
||||||
// Only run osc633 if we are in vscode
|
// Only run osc633 if we are in vscode
|
||||||
if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) {
|
if stack
|
||||||
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
||||||
|
.and_then(|v| v.as_str().ok())
|
||||||
|
== Some("vscode")
|
||||||
|
{
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
run_ansi_sequence(&get_command_finished_marker(
|
run_ansi_sequence(&get_command_finished_marker(
|
||||||
@ -1331,10 +1358,9 @@ fn run_finaliziation_ansi_sequence(
|
|||||||
|
|
||||||
// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
|
// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
static DRIVE_PATH_REGEX: once_cell::sync::Lazy<fancy_regex::Regex> =
|
static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
|
||||||
once_cell::sync::Lazy::new(|| {
|
fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
|
||||||
fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// A best-effort "does this string look kinda like a path?" function to determine whether to auto-cd
|
// 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 {
|
fn looks_like_path(orig: &str) -> bool {
|
||||||
@ -1378,8 +1404,7 @@ fn trailing_slash_looks_like_path() {
|
|||||||
fn are_session_ids_in_sync() {
|
fn are_session_ids_in_sync() {
|
||||||
let engine_state = &mut EngineState::new();
|
let engine_state = &mut EngineState::new();
|
||||||
let history = engine_state.history_config().unwrap();
|
let history = engine_state.history_config().unwrap();
|
||||||
let history_path =
|
let history_path = history.file_path().unwrap();
|
||||||
crate::config_files::get_history_path("nushell", history.file_format).unwrap();
|
|
||||||
let line_editor = reedline::Reedline::create();
|
let line_editor = reedline::Reedline::create();
|
||||||
let history_session_id = reedline::Reedline::create_history_session_id();
|
let history_session_id = reedline::Reedline::create_history_session_id();
|
||||||
let line_editor = update_line_editor_history(
|
let line_editor = update_line_editor_history(
|
||||||
@ -1397,7 +1422,7 @@ fn are_session_ids_in_sync() {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_auto_cd {
|
mod test_auto_cd {
|
||||||
use super::{do_auto_cd, parse_operation, ReplOperation};
|
use super::{do_auto_cd, escape_special_vscode_bytes, parse_operation, ReplOperation};
|
||||||
use nu_path::AbsolutePath;
|
use nu_path::AbsolutePath;
|
||||||
use nu_protocol::engine::{EngineState, Stack};
|
use nu_protocol::engine::{EngineState, Stack};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
@ -1547,4 +1572,43 @@ mod test_auto_cd {
|
|||||||
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
|
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
|
||||||
check(tempdir, input, dir);
|
check(tempdir, input, dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_semicolon_test() {
|
||||||
|
let input = r#"now;is"#;
|
||||||
|
let expected = r#"now\x3Bis"#;
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_backslash_test() {
|
||||||
|
let input = r#"now\is"#;
|
||||||
|
let expected = r#"now\\is"#;
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_linefeed_test() {
|
||||||
|
let input = "now\nis";
|
||||||
|
let expected = r#"now\x0Ais"#;
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_tab_null_cr_test() {
|
||||||
|
let input = "now\t\0\ris";
|
||||||
|
let expected = r#"now\x09\x00\x0Dis"#;
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_vscode_multibyte_ok() {
|
||||||
|
let input = "now🍪is";
|
||||||
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
||||||
|
assert_eq!(input, actual);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use nu_cmd_base::hook::eval_hook;
|
use nu_cmd_base::hook::eval_hook;
|
||||||
use nu_engine::{eval_block, eval_block_with_early_return};
|
use nu_engine::{eval_block, eval_block_with_early_return};
|
||||||
use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents};
|
use nu_parser::{lex, parse, unescape_unquote_string, Token, TokenContents};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
cli_error::report_compile_error,
|
cli_error::report_compile_error,
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
@ -10,7 +10,7 @@ use nu_protocol::{
|
|||||||
};
|
};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use nu_utils::enable_vt_processing;
|
use nu_utils::enable_vt_processing;
|
||||||
use nu_utils::perf;
|
use nu_utils::{escape_quote_string, perf};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
// This will collect environment variables from std::env and adds them to a stack.
|
// This will collect environment variables from std::env and adds them to a stack.
|
||||||
@ -221,7 +221,7 @@ pub fn eval_source(
|
|||||||
report_shell_error(engine_state, &err);
|
report_shell_error(engine_state, &err);
|
||||||
let code = err.exit_code();
|
let code = err.exit_code();
|
||||||
stack.set_last_error(&err);
|
stack.set_last_error(&err);
|
||||||
code
|
code.unwrap_or(0)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -282,8 +282,22 @@ fn evaluate_source(
|
|||||||
}?;
|
}?;
|
||||||
|
|
||||||
if let PipelineData::ByteStream(..) = pipeline {
|
if let PipelineData::ByteStream(..) = pipeline {
|
||||||
pipeline.print(engine_state, stack, false, false)
|
// run the display hook on bytestreams too
|
||||||
} else if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
|
run_display_hook(engine_state, stack, pipeline, false)
|
||||||
|
} else {
|
||||||
|
run_display_hook(engine_state, stack, pipeline, true)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_display_hook(
|
||||||
|
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(
|
let pipeline = eval_hook(
|
||||||
engine_state,
|
engine_state,
|
||||||
stack,
|
stack,
|
||||||
@ -292,14 +306,11 @@ fn evaluate_source(
|
|||||||
&hook,
|
&hook,
|
||||||
"display_output",
|
"display_output",
|
||||||
)?;
|
)?;
|
||||||
pipeline.print(engine_state, stack, false, false)
|
pipeline.print(engine_state, stack, no_newline, false)
|
||||||
} else {
|
} else {
|
||||||
pipeline.print(engine_state, stack, true, false)
|
pipeline.print(engine_state, stack, no_newline, false)
|
||||||
}?;
|
}
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
296
crates/nu-cli/tests/commands/history_import.rs
Normal file
296
crates/nu-cli/tests/commands/history_import.rs
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
use nu_protocol::HistoryFileFormat;
|
||||||
|
use nu_test_support::{nu, Outcome};
|
||||||
|
use reedline::{
|
||||||
|
FileBackedHistory, History, HistoryItem, HistoryItemId, ReedlineError, SearchQuery,
|
||||||
|
SqliteBackedHistory,
|
||||||
|
};
|
||||||
|
use rstest::rstest;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
struct Test {
|
||||||
|
cfg_dir: TempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Test {
|
||||||
|
fn new(history_format: &'static str) -> Self {
|
||||||
|
let cfg_dir = tempfile::Builder::new()
|
||||||
|
.prefix("history_import_test")
|
||||||
|
.tempdir()
|
||||||
|
.unwrap();
|
||||||
|
// Assigning to $env.config.history.file_format seems to work only in startup
|
||||||
|
// configuration.
|
||||||
|
std::fs::write(
|
||||||
|
cfg_dir.path().join("env.nu"),
|
||||||
|
format!("$env.config.history.file_format = {history_format:?}"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
Self { cfg_dir }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nu(&self, cmd: impl AsRef<str>) -> Outcome {
|
||||||
|
let env = [(
|
||||||
|
"XDG_CONFIG_HOME".to_string(),
|
||||||
|
self.cfg_dir.path().to_str().unwrap().to_string(),
|
||||||
|
)];
|
||||||
|
let env_config = self.cfg_dir.path().join("env.nu");
|
||||||
|
nu!(envs: env, env_config: env_config, cmd.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_plaintext(&self) -> Result<FileBackedHistory, ReedlineError> {
|
||||||
|
FileBackedHistory::with_file(
|
||||||
|
100,
|
||||||
|
self.cfg_dir
|
||||||
|
.path()
|
||||||
|
.join("nushell")
|
||||||
|
.join(HistoryFileFormat::Plaintext.default_file_name()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_sqlite(&self) -> Result<SqliteBackedHistory, ReedlineError> {
|
||||||
|
SqliteBackedHistory::with_file(
|
||||||
|
self.cfg_dir
|
||||||
|
.path()
|
||||||
|
.join("nushell")
|
||||||
|
.join(HistoryFileFormat::Sqlite.default_file_name()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_backend(&self, format: HistoryFileFormat) -> Result<Box<dyn History>, ReedlineError> {
|
||||||
|
fn boxed(be: impl History + 'static) -> Box<dyn History> {
|
||||||
|
Box::new(be)
|
||||||
|
}
|
||||||
|
use HistoryFileFormat::*;
|
||||||
|
match format {
|
||||||
|
Plaintext => self.open_plaintext().map(boxed),
|
||||||
|
Sqlite => self.open_sqlite().map(boxed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HistorySource {
|
||||||
|
Vec(Vec<HistoryItem>),
|
||||||
|
Command(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestCase {
|
||||||
|
dst_format: HistoryFileFormat,
|
||||||
|
dst_history: Vec<HistoryItem>,
|
||||||
|
src_history: HistorySource,
|
||||||
|
want_history: Vec<HistoryItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_TEST_CASE: TestCase = TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
dst_history: Vec::new(),
|
||||||
|
src_history: HistorySource::Vec(Vec::new()),
|
||||||
|
want_history: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
impl TestCase {
|
||||||
|
fn run(self) {
|
||||||
|
use HistoryFileFormat::*;
|
||||||
|
let test = Test::new(match self.dst_format {
|
||||||
|
Plaintext => "plaintext",
|
||||||
|
Sqlite => "sqlite",
|
||||||
|
});
|
||||||
|
save_all(
|
||||||
|
&mut *test.open_backend(self.dst_format).unwrap(),
|
||||||
|
self.dst_history,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let outcome = match self.src_history {
|
||||||
|
HistorySource::Vec(src_history) => {
|
||||||
|
let src_format = match self.dst_format {
|
||||||
|
Plaintext => Sqlite,
|
||||||
|
Sqlite => Plaintext,
|
||||||
|
};
|
||||||
|
save_all(&mut *test.open_backend(src_format).unwrap(), src_history).unwrap();
|
||||||
|
test.nu("history import")
|
||||||
|
}
|
||||||
|
HistorySource::Command(cmd) => {
|
||||||
|
let mut cmd = cmd.to_string();
|
||||||
|
cmd.push_str(" | history import");
|
||||||
|
test.nu(cmd)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(outcome.status.success());
|
||||||
|
let got = query_all(&*test.open_backend(self.dst_format).unwrap()).unwrap();
|
||||||
|
|
||||||
|
// Compare just the commands first, for readability.
|
||||||
|
fn commands_only(items: &[HistoryItem]) -> Vec<&str> {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.command_line.as_str())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
assert_eq!(commands_only(&got), commands_only(&self.want_history));
|
||||||
|
// If commands match, compare full items.
|
||||||
|
assert_eq!(got, self.want_history);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_all(history: &dyn History) -> Result<Vec<HistoryItem>, ReedlineError> {
|
||||||
|
history.search(SearchQuery::everything(
|
||||||
|
reedline::SearchDirection::Forward,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_all(history: &mut dyn History, items: Vec<HistoryItem>) -> Result<(), ReedlineError> {
|
||||||
|
for item in items {
|
||||||
|
history.save(item)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_ITEM: HistoryItem = HistoryItem {
|
||||||
|
command_line: String::new(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_import_pipe_string() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
src_history: HistorySource::Command("echo bar"),
|
||||||
|
want_history: vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_import_pipe_record() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Sqlite,
|
||||||
|
src_history: HistorySource::Command("[[cwd command]; [/tmp some_command]]"),
|
||||||
|
want_history: vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "some_command".to_string(),
|
||||||
|
cwd: Some("/tmp".to_string()),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_empty_plaintext() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
src_history: HistorySource::Vec(vec![
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_empty_sqlite() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Sqlite,
|
||||||
|
src_history: HistorySource::Vec(vec![
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(2)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::plaintext(HistoryFileFormat::Plaintext)]
|
||||||
|
#[case::sqlite(HistoryFileFormat::Sqlite)]
|
||||||
|
fn to_existing(#[case] dst_format: HistoryFileFormat) {
|
||||||
|
TestCase {
|
||||||
|
dst_format,
|
||||||
|
dst_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "original-1".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "original-2".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
src_history: HistorySource::Vec(vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "new".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "original-1".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "original-2".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(2)),
|
||||||
|
command_line: "new".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
|
mod history_import;
|
||||||
mod keybindings_list;
|
mod keybindings_list;
|
||||||
mod nu_highlight;
|
mod nu_highlight;
|
||||||
|
@ -18,11 +18,11 @@ use support::{
|
|||||||
#[fixture]
|
#[fixture]
|
||||||
fn completer() -> NuCompleter {
|
fn completer() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Add record value as example
|
// Add record value as example
|
||||||
let record = "def tst [--mod -s] {}";
|
let record = "def tst [--mod -s] {}";
|
||||||
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
@ -31,11 +31,12 @@ fn completer() -> NuCompleter {
|
|||||||
#[fixture]
|
#[fixture]
|
||||||
fn completer_strings() -> NuCompleter {
|
fn completer_strings() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Add record value as example
|
// Add record value as example
|
||||||
let record = r#"def animals [] { ["cat", "dog", "eel" ] }
|
let record = r#"def animals [] { ["cat", "dog", "eel" ] }
|
||||||
def my-command [animal: string@animals] { print $animal }"#;
|
def my-command [animal: string@animals] { print $animal }"#;
|
||||||
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
@ -44,7 +45,7 @@ fn completer_strings() -> NuCompleter {
|
|||||||
#[fixture]
|
#[fixture]
|
||||||
fn extern_completer() -> NuCompleter {
|
fn extern_completer() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Add record value as example
|
// Add record value as example
|
||||||
let record = r#"
|
let record = r#"
|
||||||
@ -55,7 +56,7 @@ fn extern_completer() -> NuCompleter {
|
|||||||
-b: string@animals
|
-b: string@animals
|
||||||
]
|
]
|
||||||
"#;
|
"#;
|
||||||
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
@ -64,7 +65,7 @@ fn extern_completer() -> NuCompleter {
|
|||||||
#[fixture]
|
#[fixture]
|
||||||
fn completer_strings_with_options() -> NuCompleter {
|
fn completer_strings_with_options() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
// Add record value as example
|
// Add record value as example
|
||||||
let record = r#"
|
let record = r#"
|
||||||
# To test that the config setting has no effect on the custom completions
|
# To test that the config setting has no effect on the custom completions
|
||||||
@ -81,7 +82,7 @@ fn completer_strings_with_options() -> NuCompleter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
def my-command [animal: string@animals] { print $animal }"#;
|
def my-command [animal: string@animals] { print $animal }"#;
|
||||||
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
@ -90,7 +91,7 @@ fn completer_strings_with_options() -> NuCompleter {
|
|||||||
#[fixture]
|
#[fixture]
|
||||||
fn custom_completer() -> NuCompleter {
|
fn custom_completer() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Add record value as example
|
// Add record value as example
|
||||||
let record = r#"
|
let record = r#"
|
||||||
@ -104,7 +105,7 @@ fn custom_completer() -> NuCompleter {
|
|||||||
completer: $external_completer
|
completer: $external_completer
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
@ -113,7 +114,7 @@ fn custom_completer() -> NuCompleter {
|
|||||||
#[fixture]
|
#[fixture]
|
||||||
fn subcommand_completer() -> NuCompleter {
|
fn subcommand_completer() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
let commands = r#"
|
let commands = r#"
|
||||||
$env.config.completions.algorithm = "fuzzy"
|
$env.config.completions.algorithm = "fuzzy"
|
||||||
@ -123,7 +124,7 @@ fn subcommand_completer() -> NuCompleter {
|
|||||||
def "foo aabcrr" [] {}
|
def "foo aabcrr" [] {}
|
||||||
def food [] {}
|
def food [] {}
|
||||||
"#;
|
"#;
|
||||||
assert!(support::merge_input(commands.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(commands.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
@ -133,13 +134,13 @@ fn subcommand_completer() -> NuCompleter {
|
|||||||
#[fixture]
|
#[fixture]
|
||||||
fn fuzzy_alpha_sort_completer() -> NuCompleter {
|
fn fuzzy_alpha_sort_completer() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
let config = r#"
|
let config = r#"
|
||||||
$env.config.completions.algorithm = "fuzzy"
|
$env.config.completions.algorithm = "fuzzy"
|
||||||
$env.config.completions.sort = "alphabetical"
|
$env.config.completions.sort = "alphabetical"
|
||||||
"#;
|
"#;
|
||||||
assert!(support::merge_input(config.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(config.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
NuCompleter::new(Arc::new(engine), Arc::new(stack))
|
||||||
@ -942,7 +943,7 @@ fn flag_completions() {
|
|||||||
// Test completions for the 'ls' flags
|
// Test completions for the 'ls' flags
|
||||||
let suggestions = completer.complete("ls -", 4);
|
let suggestions = completer.complete("ls -", 4);
|
||||||
|
|
||||||
assert_eq!(16, suggestions.len());
|
assert_eq!(18, suggestions.len());
|
||||||
|
|
||||||
let expected: Vec<String> = vec![
|
let expected: Vec<String> = vec![
|
||||||
"--all".into(),
|
"--all".into(),
|
||||||
@ -953,6 +954,7 @@ fn flag_completions() {
|
|||||||
"--long".into(),
|
"--long".into(),
|
||||||
"--mime-type".into(),
|
"--mime-type".into(),
|
||||||
"--short-names".into(),
|
"--short-names".into(),
|
||||||
|
"--threads".into(),
|
||||||
"-D".into(),
|
"-D".into(),
|
||||||
"-a".into(),
|
"-a".into(),
|
||||||
"-d".into(),
|
"-d".into(),
|
||||||
@ -961,6 +963,7 @@ fn flag_completions() {
|
|||||||
"-l".into(),
|
"-l".into(),
|
||||||
"-m".into(),
|
"-m".into(),
|
||||||
"-s".into(),
|
"-s".into(),
|
||||||
|
"-t".into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Match results
|
// Match results
|
||||||
@ -1194,11 +1197,11 @@ fn folder_with_directorycompletions_do_not_collapse_dots() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn variables_completions() {
|
fn variables_completions() {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Add record value as example
|
// Add record value as example
|
||||||
let record = "let actor = { name: 'Tom Hardy', age: 44 }";
|
let record = "let actor = { name: 'Tom Hardy', age: 44 }";
|
||||||
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
@ -1309,11 +1312,11 @@ fn variables_completions() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alias_of_command_and_flags() {
|
fn alias_of_command_and_flags() {
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Create an alias
|
// Create an alias
|
||||||
let alias = r#"alias ll = ls -l"#;
|
let alias = r#"alias ll = ls -l"#;
|
||||||
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
@ -1328,11 +1331,11 @@ fn alias_of_command_and_flags() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alias_of_basic_command() {
|
fn alias_of_basic_command() {
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Create an alias
|
// Create an alias
|
||||||
let alias = r#"alias ll = ls "#;
|
let alias = r#"alias ll = ls "#;
|
||||||
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
@ -1347,14 +1350,14 @@ fn alias_of_basic_command() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alias_of_another_alias() {
|
fn alias_of_another_alias() {
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Create an alias
|
// Create an alias
|
||||||
let alias = r#"alias ll = ls -la"#;
|
let alias = r#"alias ll = ls -la"#;
|
||||||
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir.clone()).is_ok());
|
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
// Create the second alias
|
// Create the second alias
|
||||||
let alias = r#"alias lf = ll -f"#;
|
let alias = r#"alias lf = ll -f"#;
|
||||||
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
@ -1371,7 +1374,7 @@ fn run_external_completion(completer: &str, input: &str) -> Vec<Suggestion> {
|
|||||||
let completer = format!("$env.config.completions.external.completer = {completer}");
|
let completer = format!("$env.config.completions.external.completer = {completer}");
|
||||||
|
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
let (dir, _, mut engine_state, mut stack) = new_engine();
|
let (_, _, mut engine_state, mut stack) = new_engine();
|
||||||
let (block, delta) = {
|
let (block, delta) = {
|
||||||
let mut working_set = StateWorkingSet::new(&engine_state);
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||||
let block = parse(&mut working_set, None, completer.as_bytes(), false);
|
let block = parse(&mut working_set, None, completer.as_bytes(), false);
|
||||||
@ -1387,7 +1390,7 @@ fn run_external_completion(completer: &str, input: &str) -> Vec<Suggestion> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Merge environment into the permanent state
|
// Merge environment into the permanent state
|
||||||
assert!(engine_state.merge_env(&mut stack, &dir).is_ok());
|
assert!(engine_state.merge_env(&mut stack).is_ok());
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine_state), Arc::new(stack));
|
let mut completer = NuCompleter::new(Arc::new(engine_state), Arc::new(stack));
|
||||||
@ -1576,11 +1579,11 @@ fn sort_fuzzy_completions_in_alphabetical_order(mut fuzzy_alpha_sort_completer:
|
|||||||
#[ignore = "was reverted, still needs fixing"]
|
#[ignore = "was reverted, still needs fixing"]
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn alias_offset_bug_7648() {
|
fn alias_offset_bug_7648() {
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Create an alias
|
// Create an alias
|
||||||
let alias = r#"alias ea = ^$env.EDITOR /tmp/test.s"#;
|
let alias = r#"alias ea = ^$env.EDITOR /tmp/test.s"#;
|
||||||
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
@ -1595,11 +1598,11 @@ fn alias_offset_bug_7648() {
|
|||||||
#[ignore = "was reverted, still needs fixing"]
|
#[ignore = "was reverted, still needs fixing"]
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn alias_offset_bug_7754() {
|
fn alias_offset_bug_7754() {
|
||||||
let (dir, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
|
||||||
// Create an alias
|
// Create an alias
|
||||||
let alias = r#"alias ll = ls -l"#;
|
let alias = r#"alias ll = ls -l"#;
|
||||||
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
|
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Merge environment into the permanent state
|
// Merge environment into the permanent state
|
||||||
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
let merge_result = engine_state.merge_env(&mut stack);
|
||||||
assert!(merge_result.is_ok());
|
assert!(merge_result.is_ok());
|
||||||
|
|
||||||
(dir, dir_str, engine_state, stack)
|
(dir, dir_str, engine_state, stack)
|
||||||
@ -109,7 +109,7 @@ pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Merge environment into the permanent state
|
// Merge environment into the permanent state
|
||||||
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
let merge_result = engine_state.merge_env(&mut stack);
|
||||||
assert!(merge_result.is_ok());
|
assert!(merge_result.is_ok());
|
||||||
|
|
||||||
(dir, dir_str, engine_state, stack)
|
(dir, dir_str, engine_state, stack)
|
||||||
@ -144,7 +144,7 @@ pub fn new_quote_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Merge environment into the permanent state
|
// Merge environment into the permanent state
|
||||||
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
let merge_result = engine_state.merge_env(&mut stack);
|
||||||
assert!(merge_result.is_ok());
|
assert!(merge_result.is_ok());
|
||||||
|
|
||||||
(dir, dir_str, engine_state, stack)
|
(dir, dir_str, engine_state, stack)
|
||||||
@ -179,7 +179,7 @@ pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Merge environment into the permanent state
|
// Merge environment into the permanent state
|
||||||
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
let merge_result = engine_state.merge_env(&mut stack);
|
||||||
assert!(merge_result.is_ok());
|
assert!(merge_result.is_ok());
|
||||||
|
|
||||||
(dir, dir_str, engine_state, stack)
|
(dir, dir_str, engine_state, stack)
|
||||||
@ -223,7 +223,6 @@ pub fn merge_input(
|
|||||||
input: &[u8],
|
input: &[u8],
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
dir: AbsolutePathBuf,
|
|
||||||
) -> Result<(), ShellError> {
|
) -> Result<(), ShellError> {
|
||||||
let (block, delta) = {
|
let (block, delta) = {
|
||||||
let mut working_set = StateWorkingSet::new(engine_state);
|
let mut working_set = StateWorkingSet::new(engine_state);
|
||||||
@ -246,5 +245,5 @@ pub fn merge_input(
|
|||||||
.is_ok());
|
.is_ok());
|
||||||
|
|
||||||
// Merge environment into the permanent state
|
// Merge environment into the permanent state
|
||||||
engine_state.merge_env(stack, &dir)
|
engine_state.merge_env(stack)
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cmd-base"
|
name = "nu-cmd-base"
|
||||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
|
||||||
version = "0.98.0"
|
version = "0.100.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -13,10 +13,10 @@ version = "0.98.0"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-engine = { path = "../nu-engine", version = "0.98.0" }
|
nu-engine = { path = "../nu-engine", version = "0.100.0" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.98.0" }
|
nu-parser = { path = "../nu-parser", version = "0.100.0" }
|
||||||
nu-path = { path = "../nu-path", version = "0.98.0" }
|
nu-path = { path = "../nu-path", version = "0.100.0" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.98.0" }
|
nu-protocol = { path = "../nu-protocol", version = "0.100.0" }
|
||||||
|
|
||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
miette = { workspace = true }
|
miette = { workspace = true }
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use crate::util::get_guaranteed_cwd;
|
|
||||||
use miette::Result;
|
use miette::Result;
|
||||||
use nu_engine::{eval_block, eval_block_with_early_return};
|
use nu_engine::{eval_block, eval_block_with_early_return};
|
||||||
use nu_parser::parse;
|
use nu_parser::parse;
|
||||||
@ -19,17 +18,12 @@ pub fn eval_env_change_hook(
|
|||||||
match hook {
|
match hook {
|
||||||
Value::Record { val, .. } => {
|
Value::Record { val, .. } => {
|
||||||
for (env_name, hook_value) in &*val {
|
for (env_name, hook_value) in &*val {
|
||||||
let before = engine_state
|
let before = engine_state.previous_env_vars.get(env_name);
|
||||||
.previous_env_vars
|
let after = stack.get_env_var(engine_state, env_name);
|
||||||
.get(env_name)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let after = stack
|
|
||||||
.get_env_var(engine_state, env_name)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if before != after {
|
if before != after {
|
||||||
|
let before = before.cloned().unwrap_or_default();
|
||||||
|
let after = after.cloned().unwrap_or_default();
|
||||||
|
|
||||||
eval_hook(
|
eval_hook(
|
||||||
engine_state,
|
engine_state,
|
||||||
stack,
|
stack,
|
||||||
@ -40,7 +34,7 @@ pub fn eval_env_change_hook(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
Arc::make_mut(&mut engine_state.previous_env_vars)
|
Arc::make_mut(&mut engine_state.previous_env_vars)
|
||||||
.insert(env_name.to_string(), after);
|
.insert(env_name.clone(), after);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,11 +86,12 @@ pub fn eval_hook(
|
|||||||
);
|
);
|
||||||
if let Some(err) = working_set.parse_errors.first() {
|
if let Some(err) = working_set.parse_errors.first() {
|
||||||
report_parse_error(&working_set, err);
|
report_parse_error(&working_set, err);
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
error: format!("Failed to run {hook_name} hook"),
|
||||||
expected: "valid source code".into(),
|
msg: "source code has errors".into(),
|
||||||
value: "source code with syntax errors".into(),
|
span: Some(span),
|
||||||
span,
|
help: None,
|
||||||
|
inner: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,10 +162,10 @@ pub fn eval_hook(
|
|||||||
{
|
{
|
||||||
val
|
val
|
||||||
} else {
|
} else {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
return Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "boolean output".to_string(),
|
expected: Type::Bool,
|
||||||
value: "other PipelineData variant".to_string(),
|
actual: pipeline_data.get_type(),
|
||||||
span: other_span,
|
span: pipeline_data.span().unwrap_or(other_span),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,9 +174,9 @@ pub fn eval_hook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
return Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "block".to_string(),
|
expected: Type::Closure,
|
||||||
value: format!("{}", condition.get_type()),
|
actual: condition.get_type(),
|
||||||
span: other_span,
|
span: other_span,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -224,11 +219,12 @@ pub fn eval_hook(
|
|||||||
);
|
);
|
||||||
if let Some(err) = working_set.parse_errors.first() {
|
if let Some(err) = working_set.parse_errors.first() {
|
||||||
report_parse_error(&working_set, err);
|
report_parse_error(&working_set, err);
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
error: format!("Failed to run {hook_name} hook"),
|
||||||
expected: "valid source code".into(),
|
msg: "source code has errors".into(),
|
||||||
value: "source code with syntax errors".into(),
|
span: Some(span),
|
||||||
span: source_span,
|
help: None,
|
||||||
|
inner: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,9 +259,9 @@ pub fn eval_hook(
|
|||||||
run_hook(engine_state, stack, val, input, arguments, source_span)?;
|
run_hook(engine_state, stack, val, input, arguments, source_span)?;
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
return Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "block or string".to_string(),
|
expected: Type::custom("string or closure"),
|
||||||
value: format!("{}", other.get_type()),
|
actual: other.get_type(),
|
||||||
span: source_span,
|
span: source_span,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -276,16 +272,15 @@ pub fn eval_hook(
|
|||||||
output = run_hook(engine_state, stack, val, input, arguments, span)?;
|
output = run_hook(engine_state, stack, val, input, arguments, span)?;
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
return Err(ShellError::UnsupportedConfigValue {
|
return Err(ShellError::RuntimeTypeMismatch {
|
||||||
expected: "string, block, record, or list of commands".into(),
|
expected: Type::custom("string, closure, record, or list"),
|
||||||
value: format!("{}", other.get_type()),
|
actual: other.get_type(),
|
||||||
span: other.span(),
|
span: other.span(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cwd = get_guaranteed_cwd(engine_state, stack);
|
engine_state.merge_env(stack)?;
|
||||||
engine_state.merge_env(stack, cwd)?;
|
|
||||||
|
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,9 @@
|
|||||||
use nu_path::AbsolutePathBuf;
|
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack},
|
engine::{EngineState, Stack},
|
||||||
Range, ShellError, Span, Value,
|
Range, ShellError, Span, Value,
|
||||||
};
|
};
|
||||||
use std::ops::Bound;
|
use std::ops::Bound;
|
||||||
|
|
||||||
pub fn get_init_cwd() -> AbsolutePathBuf {
|
|
||||||
std::env::current_dir()
|
|
||||||
.ok()
|
|
||||||
.and_then(|path| AbsolutePathBuf::try_from(path).ok())
|
|
||||||
.or_else(|| {
|
|
||||||
std::env::var("PWD")
|
|
||||||
.ok()
|
|
||||||
.and_then(|path| AbsolutePathBuf::try_from(path).ok())
|
|
||||||
})
|
|
||||||
.or_else(nu_path::home_dir)
|
|
||||||
.expect("Failed to get current working directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> AbsolutePathBuf {
|
|
||||||
engine_state
|
|
||||||
.cwd(Some(stack))
|
|
||||||
.ok()
|
|
||||||
.unwrap_or_else(get_init_cwd)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MakeRangeError = fn(&str, Span) -> ShellError;
|
type MakeRangeError = fn(&str, Span) -> ShellError;
|
||||||
|
|
||||||
/// Returns a inclusive pair of boundary in given `range`.
|
/// Returns a inclusive pair of boundary in given `range`.
|
||||||
@ -99,10 +78,10 @@ pub fn get_editor(
|
|||||||
get_editor_commandline(&config.buffer_editor, "$env.config.buffer_editor")
|
get_editor_commandline(&config.buffer_editor, "$env.config.buffer_editor")
|
||||||
{
|
{
|
||||||
Ok(buff_editor)
|
Ok(buff_editor)
|
||||||
} else if let Some(value) = env_vars.get("EDITOR") {
|
|
||||||
get_editor_commandline(value, "$env.EDITOR")
|
|
||||||
} else if let Some(value) = env_vars.get("VISUAL") {
|
} else if let Some(value) = env_vars.get("VISUAL") {
|
||||||
get_editor_commandline(value, "$env.VISUAL")
|
get_editor_commandline(value, "$env.VISUAL")
|
||||||
|
} else if let Some(value) = env_vars.get("EDITOR") {
|
||||||
|
get_editor_commandline(value, "$env.EDITOR")
|
||||||
} else {
|
} else {
|
||||||
Err(ShellError::GenericError {
|
Err(ShellError::GenericError {
|
||||||
error: "No editor configured".into(),
|
error: "No editor configured".into(),
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cmd-extra"
|
name = "nu-cmd-extra"
|
||||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
|
||||||
version = "0.98.0"
|
version = "0.100.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -16,13 +16,13 @@ bench = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.98.0" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.100.0" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.98.0" }
|
nu-engine = { path = "../nu-engine", version = "0.100.0" }
|
||||||
nu-json = { version = "0.98.0", path = "../nu-json" }
|
nu-json = { version = "0.100.0", path = "../nu-json" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.98.0" }
|
nu-parser = { path = "../nu-parser", version = "0.100.0" }
|
||||||
nu-pretty-hex = { version = "0.98.0", path = "../nu-pretty-hex" }
|
nu-pretty-hex = { version = "0.100.0", path = "../nu-pretty-hex" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.98.0" }
|
nu-protocol = { path = "../nu-protocol", version = "0.100.0" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.98.0" }
|
nu-utils = { path = "../nu-utils", version = "0.100.0" }
|
||||||
|
|
||||||
# Potential dependencies for extras
|
# Potential dependencies for extras
|
||||||
heck = { workspace = true }
|
heck = { workspace = true }
|
||||||
@ -36,6 +36,6 @@ v_htmlescape = { workspace = true }
|
|||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.98.0" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.100.0" }
|
||||||
nu-command = { path = "../nu-command", version = "0.98.0" }
|
nu-command = { path = "../nu-command", version = "0.100.0" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.98.0" }
|
nu-test-support = { path = "../nu-test-support", version = "0.100.0" }
|
@ -6,7 +6,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cmd-lang"
|
name = "nu-cmd-lang"
|
||||||
version = "0.98.0"
|
version = "0.100.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
bench = false
|
bench = false
|
||||||
@ -15,16 +15,16 @@ bench = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-engine = { path = "../nu-engine", version = "0.98.0" }
|
nu-engine = { path = "../nu-engine", version = "0.100.0" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.98.0" }
|
nu-parser = { path = "../nu-parser", version = "0.100.0" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.98.0" }
|
nu-protocol = { path = "../nu-protocol", version = "0.100.0" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.98.0" }
|
nu-utils = { path = "../nu-utils", version = "0.100.0" }
|
||||||
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
shadow-rs = { version = "0.34", default-features = false }
|
shadow-rs = { version = "0.35", default-features = false }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
shadow-rs = { version = "0.34", default-features = false }
|
shadow-rs = { version = "0.35", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
mimalloc = []
|
mimalloc = []
|
||||||
|
@ -147,6 +147,7 @@ impl Command for Do {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
child.ignore_error(false);
|
||||||
child.wait()?;
|
child.wait()?;
|
||||||
|
|
||||||
let mut child = ChildProcess::from_raw(None, None, None, span);
|
let mut child = ChildProcess::from_raw(None, None, None, span);
|
||||||
@ -166,10 +167,13 @@ impl Command for Do {
|
|||||||
}
|
}
|
||||||
Ok(PipelineData::ByteStream(mut stream, metadata))
|
Ok(PipelineData::ByteStream(mut stream, metadata))
|
||||||
if ignore_program_errors
|
if ignore_program_errors
|
||||||
&& !matches!(caller_stack.stdout(), OutDest::Pipe | OutDest::Capture) =>
|
&& !matches!(
|
||||||
|
caller_stack.stdout(),
|
||||||
|
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value
|
||||||
|
) =>
|
||||||
{
|
{
|
||||||
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
||||||
child.set_exit_code(0)
|
child.ignore_error(true);
|
||||||
}
|
}
|
||||||
Ok(PipelineData::ByteStream(stream, metadata))
|
Ok(PipelineData::ByteStream(stream, metadata))
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ impl Command for HideEnv {
|
|||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![Example {
|
vec![Example {
|
||||||
description: "Hide an environment variable",
|
description: "Hide an environment variable",
|
||||||
example: r#"$env.HZ_ENV_ABC = 1; hide-env HZ_ENV_ABC; 'HZ_ENV_ABC' in (env).name"#,
|
example: r#"$env.HZ_ENV_ABC = 1; hide-env HZ_ENV_ABC; 'HZ_ENV_ABC' in $env"#,
|
||||||
result: Some(Value::test_bool(false)),
|
result: Some(Value::test_bool(false)),
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::{engine::StateWorkingSet, OutDest};
|
use nu_protocol::{engine::StateWorkingSet, ByteStreamSource, OutDest};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Ignore;
|
pub struct Ignore;
|
||||||
@ -32,8 +32,13 @@ impl Command for Ignore {
|
|||||||
_engine_state: &EngineState,
|
_engine_state: &EngineState,
|
||||||
_stack: &mut Stack,
|
_stack: &mut Stack,
|
||||||
_call: &Call,
|
_call: &Call,
|
||||||
input: PipelineData,
|
mut input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
if let PipelineData::ByteStream(stream, _) = &mut input {
|
||||||
|
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
||||||
|
child.ignore_error(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
input.drain()?;
|
input.drain()?;
|
||||||
Ok(PipelineData::empty())
|
Ok(PipelineData::empty())
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ impl Command for Let {
|
|||||||
|
|
||||||
let block = engine_state.get_block(block_id);
|
let block = engine_state.get_block(block_id);
|
||||||
let eval_block = get_eval_block(engine_state);
|
let eval_block = get_eval_block(engine_state);
|
||||||
let stack = &mut stack.start_capture();
|
let stack = &mut stack.start_collect_value();
|
||||||
let pipeline_data = eval_block(engine_state, stack, block, input)?;
|
let pipeline_data = eval_block(engine_state, stack, block, input)?;
|
||||||
let value = pipeline_data.into_value(call.head)?;
|
let value = pipeline_data.into_value(call.head)?;
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ impl Command for Mut {
|
|||||||
|
|
||||||
let block = engine_state.get_block(block_id);
|
let block = engine_state.get_block(block_id);
|
||||||
let eval_block = get_eval_block(engine_state);
|
let eval_block = get_eval_block(engine_state);
|
||||||
let stack = &mut stack.start_capture();
|
let stack = &mut stack.start_collect_value();
|
||||||
let pipeline_data = eval_block(engine_state, stack, block, input)?;
|
let pipeline_data = eval_block(engine_state, stack, block, input)?;
|
||||||
let value = pipeline_data.into_value(call.head)?;
|
let value = pipeline_data.into_value(call.head)?;
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ use nu_engine::{
|
|||||||
command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block, redirect_env,
|
command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block, redirect_env,
|
||||||
};
|
};
|
||||||
use nu_parser::trim_quotes_str;
|
use nu_parser::trim_quotes_str;
|
||||||
use nu_protocol::{ast::Expr, engine::CommandType};
|
use nu_protocol::{ast::Expr, engine::CommandType, ModuleId};
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ impl Command for OverlayUse {
|
|||||||
let mut name_arg: Spanned<String> = call.req(engine_state, caller_stack, 0)?;
|
let mut name_arg: Spanned<String> = call.req(engine_state, caller_stack, 0)?;
|
||||||
name_arg.item = trim_quotes_str(&name_arg.item).to_string();
|
name_arg.item = trim_quotes_str(&name_arg.item).to_string();
|
||||||
|
|
||||||
let maybe_origin_module_id =
|
let maybe_origin_module_id: Option<ModuleId> =
|
||||||
if let Some(overlay_expr) = call.get_parser_info(caller_stack, "overlay_expr") {
|
if let Some(overlay_expr) = call.get_parser_info(caller_stack, "overlay_expr") {
|
||||||
if let Expr::Overlay(module_id) = &overlay_expr.expr {
|
if let Expr::Overlay(module_id) = &overlay_expr.expr {
|
||||||
*module_id
|
*module_id
|
||||||
|
@ -63,7 +63,7 @@ impl Command for Try {
|
|||||||
let eval_block = get_eval_block(engine_state);
|
let eval_block = get_eval_block(engine_state);
|
||||||
|
|
||||||
let result = eval_block(engine_state, stack, try_block, input)
|
let result = eval_block(engine_state, stack, try_block, input)
|
||||||
.and_then(|pipeline| pipeline.write_to_out_dests(engine_state, stack));
|
.and_then(|pipeline| pipeline.drain_to_out_dests(engine_state, stack));
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Err(err) => run_catch(err, head, catch_block, engine_state, stack, eval_block),
|
Err(err) => run_catch(err, head, catch_block, engine_state, stack, eval_block),
|
||||||
@ -107,7 +107,11 @@ fn run_catch(
|
|||||||
|
|
||||||
if let Some(catch) = catch {
|
if let Some(catch) = catch {
|
||||||
stack.set_last_error(&error);
|
stack.set_last_error(&error);
|
||||||
let error = error.into_value(span);
|
let fancy_errors = match engine_state.get_config().error_style {
|
||||||
|
nu_protocol::ErrorStyle::Fancy => true,
|
||||||
|
nu_protocol::ErrorStyle::Plain => false,
|
||||||
|
};
|
||||||
|
let error = error.into_value(span, fancy_errors);
|
||||||
let block = engine_state.get_block(catch.block_id);
|
let block = engine_state.get_block(catch.block_id);
|
||||||
// Put the error value in the positional closure var
|
// Put the error value in the positional closure var
|
||||||
if let Some(var) = block.signature.get_positional(0) {
|
if let Some(var) = block.signature.get_positional(0) {
|
||||||
|
@ -98,15 +98,21 @@ This command is a parser keyword. For details, check:
|
|||||||
engine_state.get_span_contents(import_pattern.head.span),
|
engine_state.get_span_contents(import_pattern.head.span),
|
||||||
);
|
);
|
||||||
|
|
||||||
let maybe_file_path = find_in_dirs_env(
|
let maybe_file_path_or_dir = find_in_dirs_env(
|
||||||
&module_arg_str,
|
&module_arg_str,
|
||||||
engine_state,
|
engine_state,
|
||||||
caller_stack,
|
caller_stack,
|
||||||
get_dirs_var_from_call(caller_stack, call),
|
get_dirs_var_from_call(caller_stack, call),
|
||||||
)?;
|
)?;
|
||||||
let maybe_parent = maybe_file_path
|
// module_arg_str maybe a directory, in this case
|
||||||
.as_ref()
|
// find_in_dirs_env returns a directory.
|
||||||
.and_then(|path| path.parent().map(|p| p.to_path_buf()));
|
let maybe_parent = maybe_file_path_or_dir.as_ref().and_then(|path| {
|
||||||
|
if path.is_dir() {
|
||||||
|
Some(path.to_path_buf())
|
||||||
|
} else {
|
||||||
|
path.parent().map(|p| p.to_path_buf())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut callee_stack = caller_stack
|
let mut callee_stack = caller_stack
|
||||||
.gather_captures(engine_state, &block.captures)
|
.gather_captures(engine_state, &block.captures)
|
||||||
@ -118,9 +124,15 @@ This command is a parser keyword. For details, check:
|
|||||||
callee_stack.add_env_var("FILE_PWD".to_string(), file_pwd);
|
callee_stack.add_env_var("FILE_PWD".to_string(), file_pwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(file_path) = maybe_file_path {
|
if let Some(path) = maybe_file_path_or_dir {
|
||||||
let file_path = Value::string(file_path.to_string_lossy(), call.head);
|
let module_file_path = if path.is_dir() {
|
||||||
callee_stack.add_env_var("CURRENT_FILE".to_string(), file_path);
|
// the existence of `mod.nu` is verified in parsing time
|
||||||
|
// so it's safe to use it here.
|
||||||
|
Value::string(path.join("mod.nu").to_string_lossy(), call.head)
|
||||||
|
} else {
|
||||||
|
Value::string(path.to_string_lossy(), call.head)
|
||||||
|
};
|
||||||
|
callee_stack.add_env_var("CURRENT_FILE".to_string(), module_file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let eval_block = get_eval_block(engine_state);
|
let eval_block = get_eval_block(engine_state);
|
||||||
|
@ -126,7 +126,7 @@ pub fn eval_block(
|
|||||||
cwd: &std::path::Path,
|
cwd: &std::path::Path,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let mut stack = Stack::new().capture();
|
let mut stack = Stack::new().collect_value();
|
||||||
|
|
||||||
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
|
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
|
||||||
|
|
||||||
@ -143,13 +143,13 @@ pub fn check_example_evaluates_to_expected_output(
|
|||||||
cwd: &std::path::Path,
|
cwd: &std::path::Path,
|
||||||
engine_state: &mut Box<EngineState>,
|
engine_state: &mut Box<EngineState>,
|
||||||
) {
|
) {
|
||||||
let mut stack = Stack::new().capture();
|
let mut stack = Stack::new().collect_value();
|
||||||
|
|
||||||
// Set up PWD
|
// Set up PWD
|
||||||
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
|
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
|
||||||
|
|
||||||
engine_state
|
engine_state
|
||||||
.merge_env(&mut stack, cwd)
|
.merge_env(&mut stack)
|
||||||
.expect("Error merging environment");
|
.expect("Error merging environment");
|
||||||
|
|
||||||
let empty_input = PipelineData::empty();
|
let empty_input = PipelineData::empty();
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-cmd-plugin"
|
name = "nu-cmd-plugin"
|
||||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
|
||||||
version = "0.98.0"
|
version = "0.100.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -13,10 +13,10 @@ version = "0.98.0"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-engine = { path = "../nu-engine", version = "0.98.0" }
|
nu-engine = { path = "../nu-engine", version = "0.100.0" }
|
||||||
nu-path = { path = "../nu-path", version = "0.98.0" }
|
nu-path = { path = "../nu-path", version = "0.100.0" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.98.0", features = ["plugin"] }
|
nu-protocol = { path = "../nu-protocol", version = "0.100.0", features = ["plugin"] }
|
||||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.98.0" }
|
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.100.0" }
|
||||||
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
|
use crate::util::{get_plugin_dirs, modify_plugin_file};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_plugin_engine::{GetPlugin, PersistentPlugin};
|
use nu_plugin_engine::{GetPlugin, PersistentPlugin};
|
||||||
use nu_protocol::{PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin};
|
use nu_protocol::{PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::util::{get_plugin_dirs, modify_plugin_file};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PluginAdd;
|
pub struct PluginAdd;
|
||||||
|
|
||||||
@ -31,7 +30,7 @@ impl Command for PluginAdd {
|
|||||||
)
|
)
|
||||||
.required(
|
.required(
|
||||||
"filename",
|
"filename",
|
||||||
SyntaxShape::Filepath,
|
SyntaxShape::String,
|
||||||
"Path to the executable for the plugin",
|
"Path to the executable for the plugin",
|
||||||
)
|
)
|
||||||
.category(Category::Plugin)
|
.category(Category::Plugin)
|
||||||
@ -81,7 +80,6 @@ apparent the next time `nu` is next launched with that plugin registry file.
|
|||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let filename: Spanned<String> = call.req(engine_state, stack, 0)?;
|
let filename: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||||
let shell: Option<Spanned<String>> = call.get_flag(engine_state, stack, "shell")?;
|
let shell: Option<Spanned<String>> = call.get_flag(engine_state, stack, "shell")?;
|
||||||
|
|
||||||
let cwd = engine_state.cwd(Some(stack))?;
|
let cwd = engine_state.cwd(Some(stack))?;
|
||||||
|
|
||||||
// Check the current directory, or fall back to NU_PLUGIN_DIRS
|
// Check the current directory, or fall back to NU_PLUGIN_DIRS
|
||||||
@ -121,7 +119,7 @@ apparent the next time `nu` is next launched with that plugin registry file.
|
|||||||
let metadata = interface.get_metadata()?;
|
let metadata = interface.get_metadata()?;
|
||||||
let commands = interface.get_signature()?;
|
let commands = interface.get_signature()?;
|
||||||
|
|
||||||
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
modify_plugin_file(engine_state, stack, call.head, &custom_path, |contents| {
|
||||||
// Update the file with the received metadata and signatures
|
// Update the file with the received metadata and signatures
|
||||||
let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
|
let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
|
||||||
contents.upsert_plugin(item);
|
contents.upsert_plugin(item);
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use itertools::Itertools;
|
use itertools::{EitherOrBoth, Itertools};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{IntoValue, PluginRegistryItemData};
|
||||||
|
|
||||||
|
use crate::util::read_plugin_file;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PluginList;
|
pub struct PluginList;
|
||||||
@ -17,7 +20,7 @@ impl Command for PluginList {
|
|||||||
[
|
[
|
||||||
("name".into(), Type::String),
|
("name".into(), Type::String),
|
||||||
("version".into(), Type::String),
|
("version".into(), Type::String),
|
||||||
("is_running".into(), Type::Bool),
|
("status".into(), Type::String),
|
||||||
("pid".into(), Type::Int),
|
("pid".into(), Type::Int),
|
||||||
("filename".into(), Type::String),
|
("filename".into(), Type::String),
|
||||||
("shell".into(), Type::String),
|
("shell".into(), Type::String),
|
||||||
@ -26,11 +29,54 @@ impl Command for PluginList {
|
|||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.named(
|
||||||
|
"plugin-config",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"Use a plugin registry file other than the one set in `$nu.plugin-path`",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"engine",
|
||||||
|
"Show info for plugins that are loaded into the engine only.",
|
||||||
|
Some('e'),
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"registry",
|
||||||
|
"Show info for plugins from the registry file only.",
|
||||||
|
Some('r'),
|
||||||
|
)
|
||||||
.category(Category::Plugin)
|
.category(Category::Plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"List installed plugins."
|
"List loaded and installed plugins."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"
|
||||||
|
The `status` column will contain one of the following values:
|
||||||
|
|
||||||
|
- `added`: The plugin is present in the plugin registry file, but not in
|
||||||
|
the engine.
|
||||||
|
- `loaded`: The plugin is present both in the plugin registry file and in
|
||||||
|
the engine, but is not running.
|
||||||
|
- `running`: The plugin is currently running, and the `pid` column should
|
||||||
|
contain its process ID.
|
||||||
|
- `modified`: The plugin state present in the plugin registry file is different
|
||||||
|
from the state in the engine.
|
||||||
|
- `removed`: The plugin is still loaded in the engine, but is not present in
|
||||||
|
the plugin registry file.
|
||||||
|
- `invalid`: The data in the plugin registry file couldn't be deserialized,
|
||||||
|
and the plugin most likely needs to be added again.
|
||||||
|
|
||||||
|
`running` takes priority over any other status. Unless `--registry` is used
|
||||||
|
or the plugin has not been loaded yet, the values of `version`, `filename`,
|
||||||
|
`shell`, and `commands` reflect the values in the engine and not the ones in
|
||||||
|
the plugin registry file.
|
||||||
|
|
||||||
|
See also: `plugin use`
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_terms(&self) -> Vec<&str> {
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
@ -45,7 +91,7 @@ impl Command for PluginList {
|
|||||||
result: Some(Value::test_list(vec![Value::test_record(record! {
|
result: Some(Value::test_list(vec![Value::test_record(record! {
|
||||||
"name" => Value::test_string("inc"),
|
"name" => Value::test_string("inc"),
|
||||||
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
|
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
|
||||||
"is_running" => Value::test_bool(true),
|
"status" => Value::test_string("running"),
|
||||||
"pid" => Value::test_int(106480),
|
"pid" => Value::test_int(106480),
|
||||||
"filename" => if cfg!(windows) {
|
"filename" => if cfg!(windows) {
|
||||||
Value::test_string(r"C:\nu\plugins\nu_plugin_inc.exe")
|
Value::test_string(r"C:\nu\plugins\nu_plugin_inc.exe")
|
||||||
@ -67,58 +113,189 @@ impl Command for PluginList {
|
|||||||
fn run(
|
fn run(
|
||||||
&self,
|
&self,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
_stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
|
||||||
|
let engine_mode = call.has_flag(engine_state, stack, "engine")?;
|
||||||
|
let registry_mode = call.has_flag(engine_state, stack, "registry")?;
|
||||||
|
|
||||||
// Group plugin decls by plugin identity
|
let plugins_info = match (engine_mode, registry_mode) {
|
||||||
let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
|
// --engine and --registry together is equivalent to the default.
|
||||||
decl.plugin_identity()
|
(false, false) | (true, true) => {
|
||||||
.expect("plugin decl should have identity")
|
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
|
Ok(plugins_info.into_value(call.head).into_pipeline_data())
|
||||||
let list = engine_state.plugins().iter().map(|plugin| {
|
}
|
||||||
// Find commands that belong to the plugin
|
}
|
||||||
let commands = decls.get(plugin.identity())
|
|
||||||
.into_iter()
|
#[derive(Debug, Clone, IntoValue, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
.flat_map(|decls| {
|
struct PluginInfo {
|
||||||
decls.iter().map(|decl| Value::string(decl.name(), head))
|
name: String,
|
||||||
})
|
version: Option<String>,
|
||||||
.collect();
|
status: PluginStatus,
|
||||||
|
pid: Option<u32>,
|
||||||
let pid = plugin
|
filename: String,
|
||||||
.pid()
|
shell: Option<String>,
|
||||||
.map(|p| Value::int(p as i64, head))
|
commands: Vec<String>,
|
||||||
.unwrap_or(Value::nothing(head));
|
}
|
||||||
|
|
||||||
let shell = plugin
|
#[derive(Debug, Clone, Copy, IntoValue, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
.identity()
|
#[nu_value(rename_all = "snake_case")]
|
||||||
.shell()
|
enum PluginStatus {
|
||||||
.map(|s| Value::string(s.to_string_lossy(), head))
|
Added,
|
||||||
.unwrap_or(Value::nothing(head));
|
Loaded,
|
||||||
|
Running,
|
||||||
let metadata = plugin.metadata();
|
Modified,
|
||||||
let version = metadata
|
Removed,
|
||||||
.and_then(|m| m.version)
|
Invalid,
|
||||||
.map(|s| Value::string(s, head))
|
}
|
||||||
.unwrap_or(Value::nothing(head));
|
|
||||||
|
fn get_plugins_in_engine(engine_state: &EngineState) -> Vec<PluginInfo> {
|
||||||
let record = record! {
|
// Group plugin decls by plugin identity
|
||||||
"name" => Value::string(plugin.identity().name(), head),
|
let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
|
||||||
"version" => version,
|
decl.plugin_identity()
|
||||||
"is_running" => Value::bool(plugin.is_running(), head),
|
.expect("plugin decl should have identity")
|
||||||
"pid" => pid,
|
});
|
||||||
"filename" => Value::string(plugin.identity().filename().to_string_lossy(), head),
|
|
||||||
"shell" => shell,
|
// Build plugins list
|
||||||
"commands" => Value::list(commands, head),
|
engine_state
|
||||||
};
|
.plugins()
|
||||||
|
.iter()
|
||||||
Value::record(record, head)
|
.map(|plugin| {
|
||||||
}).collect();
|
// Find commands that belong to the plugin
|
||||||
|
let commands = decls
|
||||||
Ok(Value::list(list, head).into_pipeline_data())
|
.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);
|
let filename = canonicalize_possible_filename_arg(engine_state, stack, &name.item);
|
||||||
|
|
||||||
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
modify_plugin_file(engine_state, stack, call.head, &custom_path, |contents| {
|
||||||
if let Some(index) = contents
|
if let Some(index) = contents
|
||||||
.plugins
|
.plugins
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -6,18 +6,17 @@ use std::{
|
|||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn modify_plugin_file(
|
fn get_plugin_registry_file_path(
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
span: Span,
|
span: Span,
|
||||||
custom_path: Option<Spanned<String>>,
|
custom_path: &Option<Spanned<String>>,
|
||||||
operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>,
|
) -> Result<PathBuf, ShellError> {
|
||||||
) -> Result<(), ShellError> {
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
|
|
||||||
let plugin_registry_file_path = if let Some(ref custom_path) = custom_path {
|
if let Some(ref custom_path) = custom_path {
|
||||||
nu_path::expand_path_with(&custom_path.item, cwd, true)
|
Ok(nu_path::expand_path_with(&custom_path.item, cwd, true))
|
||||||
} else {
|
} else {
|
||||||
engine_state
|
engine_state
|
||||||
.plugin_path
|
.plugin_path
|
||||||
@ -28,8 +27,53 @@ pub(crate) fn modify_plugin_file(
|
|||||||
span: Some(span),
|
span: Some(span),
|
||||||
help: Some("you may be running `nu` with --no-config-file".into()),
|
help: Some("you may be running `nu` with --no-config-file".into()),
|
||||||
inner: vec![],
|
inner: vec![],
|
||||||
})?
|
})
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_plugin_file(
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
span: Span,
|
||||||
|
custom_path: &Option<Spanned<String>>,
|
||||||
|
) -> Result<PluginRegistryFile, ShellError> {
|
||||||
|
let plugin_registry_file_path =
|
||||||
|
get_plugin_registry_file_path(engine_state, stack, span, custom_path)?;
|
||||||
|
|
||||||
|
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
|
||||||
|
|
||||||
|
// Try to read the plugin file if it exists
|
||||||
|
if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) {
|
||||||
|
PluginRegistryFile::read_from(
|
||||||
|
File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
|
||||||
|
msg: format!(
|
||||||
|
"failed to read `{}`: {}",
|
||||||
|
plugin_registry_file_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
span: file_span,
|
||||||
|
})?,
|
||||||
|
Some(file_span),
|
||||||
|
)
|
||||||
|
} else if let Some(path) = custom_path {
|
||||||
|
Err(ShellError::FileNotFound {
|
||||||
|
file: path.item.clone(),
|
||||||
|
span: path.span,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(PluginRegistryFile::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn modify_plugin_file(
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
span: Span,
|
||||||
|
custom_path: &Option<Spanned<String>>,
|
||||||
|
operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
let plugin_registry_file_path =
|
||||||
|
get_plugin_registry_file_path(engine_state, stack, span, custom_path)?;
|
||||||
|
|
||||||
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
|
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
|
||||||
|
|
||||||
@ -95,8 +139,9 @@ pub(crate) fn get_plugin_dirs(
|
|||||||
let working_set = StateWorkingSet::new(engine_state);
|
let working_set = StateWorkingSet::new(engine_state);
|
||||||
let value = working_set
|
let value = working_set
|
||||||
.find_variable(b"$NU_PLUGIN_DIRS")
|
.find_variable(b"$NU_PLUGIN_DIRS")
|
||||||
.and_then(|var_id| working_set.get_constant(var_id).ok().cloned())
|
.and_then(|var_id| working_set.get_constant(var_id).ok())
|
||||||
.or_else(|| stack.get_env_var(engine_state, "NU_PLUGIN_DIRS"));
|
.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
|
// Get all of the strings in the list, if possible
|
||||||
value
|
value
|
||||||
|
@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-color-confi
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-color-config"
|
name = "nu-color-config"
|
||||||
version = "0.98.0"
|
version = "0.100.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
bench = false
|
bench = false
|
||||||
@ -14,12 +14,12 @@ bench = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.98.0" }
|
nu-protocol = { path = "../nu-protocol", version = "0.100.0" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.98.0" }
|
nu-engine = { path = "../nu-engine", version = "0.100.0" }
|
||||||
nu-json = { path = "../nu-json", version = "0.98.0" }
|
nu-json = { path = "../nu-json", version = "0.100.0" }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.98.0" }
|
nu-test-support = { path = "../nu-test-support", version = "0.100.0" }
|
@ -2,9 +2,8 @@ use crate::{color_record_to_nustyle, lookup_ansi_color_style, text_style::Alignm
|
|||||||
use nu_ansi_term::{Color, Style};
|
use nu_ansi_term::{Color, Style};
|
||||||
use nu_engine::ClosureEvalOnce;
|
use nu_engine::ClosureEvalOnce;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
cli_error::CliError,
|
engine::{Closure, EngineState, Stack},
|
||||||
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
report_shell_error, Span, Value,
|
||||||
Span, Value,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
@ -70,14 +69,8 @@ impl<'a> StyleComputer<'a> {
|
|||||||
_ => Style::default(),
|
_ => Style::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This is basically a copy of nu_cli::report_error(), but that isn't usable due to
|
Err(err) => {
|
||||||
// dependencies. While crudely spitting out a bunch of errors like this is not ideal,
|
report_shell_error(self.engine_state, &err);
|
||||||
// currently hook closure errors behave roughly the same.
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!(
|
|
||||||
"Error: {:?}",
|
|
||||||
CliError(&e, &StateWorkingSet::new(self.engine_state))
|
|
||||||
);
|
|
||||||
Style::default()
|
Style::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,7 +223,7 @@ fn test_computable_style_closure_basic() {
|
|||||||
];
|
];
|
||||||
let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
|
let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
|
||||||
assert_eq!(actual_repl.err, "");
|
assert_eq!(actual_repl.err, "");
|
||||||
assert_eq!(actual_repl.out, "[bell.obj, book.obj, candle.obj]");
|
assert_eq!(actual_repl.out, r#"["bell.obj", "book.obj", "candle.obj"]"#);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "nu-command"
|
name = "nu-command"
|
||||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
|
||||||
version = "0.98.0"
|
version = "0.100.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -16,21 +16,21 @@ bench = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cmd-base = { path = "../nu-cmd-base", version = "0.98.0" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.100.0" }
|
||||||
nu-color-config = { path = "../nu-color-config", version = "0.98.0" }
|
nu-color-config = { path = "../nu-color-config", version = "0.100.0" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.98.0" }
|
nu-engine = { path = "../nu-engine", version = "0.100.0" }
|
||||||
nu-glob = { path = "../nu-glob", version = "0.98.0" }
|
nu-glob = { path = "../nu-glob", version = "0.100.0" }
|
||||||
nu-json = { path = "../nu-json", version = "0.98.0" }
|
nu-json = { path = "../nu-json", version = "0.100.0" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.98.0" }
|
nu-parser = { path = "../nu-parser", version = "0.100.0" }
|
||||||
nu-path = { path = "../nu-path", version = "0.98.0" }
|
nu-path = { path = "../nu-path", version = "0.100.0" }
|
||||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.98.0" }
|
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.100.0" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.98.0" }
|
nu-protocol = { path = "../nu-protocol", version = "0.100.0" }
|
||||||
nu-system = { path = "../nu-system", version = "0.98.0" }
|
nu-system = { path = "../nu-system", version = "0.100.0" }
|
||||||
nu-table = { path = "../nu-table", version = "0.98.0" }
|
nu-table = { path = "../nu-table", version = "0.100.0" }
|
||||||
nu-term-grid = { path = "../nu-term-grid", version = "0.98.0" }
|
nu-term-grid = { path = "../nu-term-grid", version = "0.100.0" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.98.0" }
|
nu-utils = { path = "../nu-utils", version = "0.100.0" }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
nuon = { path = "../nuon", version = "0.98.0" }
|
nuon = { path = "../nuon", version = "0.100.0" }
|
||||||
|
|
||||||
alphanumeric-sort = { workspace = true }
|
alphanumeric-sort = { workspace = true }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
@ -66,7 +66,7 @@ native-tls = { workspace = true }
|
|||||||
notify-debouncer-full = { workspace = true, default-features = false }
|
notify-debouncer-full = { workspace = true, default-features = false }
|
||||||
num-format = { workspace = true }
|
num-format = { workspace = true }
|
||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
oem_cp = { workspace = true }
|
||||||
open = { workspace = true }
|
open = { workspace = true }
|
||||||
os_pipe = { workspace = true }
|
os_pipe = { workspace = true }
|
||||||
pathdiff = { workspace = true }
|
pathdiff = { workspace = true }
|
||||||
@ -139,8 +139,8 @@ sqlite = ["rusqlite"]
|
|||||||
trash-support = ["trash"]
|
trash-support = ["trash"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.98.0" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.100.0" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.98.0" }
|
nu-test-support = { path = "../nu-test-support", version = "0.100.0" }
|
||||||
|
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
mockito = { workspace = true, default-features = false }
|
mockito = { workspace = true, default-features = false }
|
||||||
|
@ -183,6 +183,7 @@ mod test {
|
|||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{CellPath, PathMember},
|
ast::{CellPath, PathMember},
|
||||||
engine::Closure,
|
engine::Closure,
|
||||||
|
BlockId,
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@ -244,7 +245,7 @@ mod test {
|
|||||||
Value::list(vec![Value::bool(true, span)], span),
|
Value::list(vec![Value::bool(true, span)], span),
|
||||||
Value::closure(
|
Value::closure(
|
||||||
Closure {
|
Closure {
|
||||||
block_id: 0,
|
block_id: BlockId::new(0),
|
||||||
captures: Vec::new(),
|
captures: Vec::new(),
|
||||||
},
|
},
|
||||||
span,
|
span,
|
||||||
|
@ -108,7 +108,7 @@ impl Command for Fill {
|
|||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description:
|
description:
|
||||||
"Fill a filesize on the left side to a width of 5 with the character '0'",
|
"Fill a filesize on both sides to a width of 10 with the character '0'",
|
||||||
example: "1kib | fill --alignment middle --character '0' --width 10",
|
example: "1kib | fill --alignment middle --character '0' --width 10",
|
||||||
result: Some(Value::string("0001024000", Span::test_data())),
|
result: Some(Value::string("0001024000", Span::test_data())),
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{generate_strftime_list, parse_date_from_string};
|
use crate::{generate_strftime_list, parse_date_from_string};
|
||||||
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc};
|
||||||
use human_date_parser::{from_human_time, ParseResult};
|
use human_date_parser::{from_human_time, ParseResult};
|
||||||
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
@ -185,11 +185,13 @@ impl Command for SubCommand {
|
|||||||
example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
|
example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
|
||||||
#[allow(clippy::inconsistent_digit_grouping)]
|
#[allow(clippy::inconsistent_digit_grouping)]
|
||||||
result: Some(Value::date(
|
result: Some(Value::date(
|
||||||
DateTime::from_naive_utc_and_offset(
|
Local
|
||||||
NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
.from_local_datetime(
|
||||||
.expect("date calculation should not fail in test"),
|
&NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
||||||
*Local::now().offset(),
|
.expect("date calculation should not fail in test"),
|
||||||
),
|
)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(Local::now().offset()),
|
||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
@ -275,12 +277,13 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
if let Ok(date) = from_human_time(&input_val) {
|
if let Ok(date) = from_human_time(&input_val) {
|
||||||
match date {
|
match date {
|
||||||
ParseResult::Date(date) => {
|
ParseResult::Date(date) => {
|
||||||
let time = NaiveTime::from_hms_opt(0, 0, 0).expect("valid time");
|
let time = Local::now().time();
|
||||||
let combined = date.and_time(time);
|
let combined = date.and_time(time);
|
||||||
let dt_fixed = DateTime::from_naive_utc_and_offset(
|
let local_offset = *Local::now().offset();
|
||||||
combined,
|
let dt_fixed =
|
||||||
*Local::now().offset(),
|
TimeZone::from_local_datetime(&local_offset, &combined)
|
||||||
);
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
return Value::date(dt_fixed, span);
|
return Value::date(dt_fixed, span);
|
||||||
}
|
}
|
||||||
ParseResult::DateTime(date) => {
|
ParseResult::DateTime(date) => {
|
||||||
@ -289,10 +292,11 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
ParseResult::Time(time) => {
|
ParseResult::Time(time) => {
|
||||||
let date = Local::now().date_naive();
|
let date = Local::now().date_naive();
|
||||||
let combined = date.and_time(time);
|
let combined = date.and_time(time);
|
||||||
let dt_fixed = DateTime::from_naive_utc_and_offset(
|
let local_offset = *Local::now().offset();
|
||||||
combined,
|
let dt_fixed =
|
||||||
*Local::now().offset(),
|
TimeZone::from_local_datetime(&local_offset, &combined)
|
||||||
);
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
return Value::date(dt_fixed, span);
|
return Value::date(dt_fixed, span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -386,13 +390,15 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
Ok(d) => Value::date ( d, head ),
|
Ok(d) => Value::date ( d, head ),
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
match NaiveDateTime::parse_from_str(val, &dt.0) {
|
match NaiveDateTime::parse_from_str(val, &dt.0) {
|
||||||
Ok(d) => Value::date (
|
Ok(d) => {
|
||||||
DateTime::from_naive_utc_and_offset(
|
let local_offset = *Local::now().offset();
|
||||||
d,
|
let dt_fixed =
|
||||||
*Local::now().offset(),
|
TimeZone::from_local_datetime(&local_offset, &d)
|
||||||
),
|
.single()
|
||||||
head,
|
.unwrap_or_default();
|
||||||
),
|
|
||||||
|
Value::date (dt_fixed,head)
|
||||||
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
Value::error (
|
Value::error (
|
||||||
ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) },
|
ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) },
|
||||||
@ -503,7 +509,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore]
|
||||||
fn takes_a_date_format_without_timezone() {
|
fn takes_a_date_format_without_timezone() {
|
||||||
|
// Ignoring this test for now because we changed the human-date-parser to use
|
||||||
|
// the users timezone instead of UTC. We may continue to tweak this behavior.
|
||||||
|
// Another hacky solution is to set the timezone to UTC in the test, which works
|
||||||
|
// on MacOS and Linux but hasn't been tested on Windows. Plus it kind of defeats
|
||||||
|
// the purpose of a "without_timezone" test.
|
||||||
|
// std::env::set_var("TZ", "UTC");
|
||||||
let date_str = Value::test_string("16.11.1984 8:00 am");
|
let date_str = Value::test_string("16.11.1984 8:00 am");
|
||||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()));
|
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()));
|
||||||
let args = Arguments {
|
let args = Arguments {
|
||||||
@ -513,12 +526,16 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let actual = action(&date_str, &args, Span::test_data());
|
let actual = action(&date_str, &args, Span::test_data());
|
||||||
let expected = Value::date(
|
let expected = Value::date(
|
||||||
DateTime::from_naive_utc_and_offset(
|
Local
|
||||||
NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P").unwrap(),
|
.from_local_datetime(
|
||||||
*Local::now().offset(),
|
&NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
||||||
),
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(Local::now().offset()),
|
||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(actual, expected)
|
assert_eq!(actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +259,7 @@ fn action(input: &Value, args: &Arguments, span: Span) -> Value {
|
|||||||
if radix == 10 {
|
if radix == 10 {
|
||||||
*val as i64
|
*val as i64
|
||||||
} else {
|
} else {
|
||||||
match convert_int(&Value::int(*val as i64, span), span, radix).as_i64() {
|
match convert_int(&Value::int(*val as i64, span), span, radix).as_int() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
_ => {
|
_ => {
|
||||||
return Value::error(
|
return Value::error(
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use crate::parse_date_from_string;
|
use crate::parse_date_from_string;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::PipelineIterator;
|
use nu_protocol::PipelineIterator;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::{Regex, RegexBuilder};
|
use regex::{Regex, RegexBuilder};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct IntoValue;
|
pub struct IntoValue;
|
||||||
@ -18,7 +18,7 @@ impl Command for IntoValue {
|
|||||||
.input_output_types(vec![(Type::table(), Type::table())])
|
.input_output_types(vec![(Type::table(), Type::table())])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table(vec![]),
|
SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
||||||
"list of columns to update",
|
"list of columns to update",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
@ -32,7 +32,11 @@ impl Command for IntoValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"Infer nushell datatype for each cell."
|
"Infer Nushell datatype for each cell."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["convert", "conversion"]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
@ -267,8 +271,9 @@ const DATETIME_DMY_PATTERN: &str = r#"(?x)
|
|||||||
$
|
$
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
static DATETIME_DMY_RE: Lazy<Regex> =
|
static DATETIME_DMY_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Lazy::new(|| Regex::new(DATETIME_DMY_PATTERN).expect("datetime_dmy_pattern should be valid"));
|
Regex::new(DATETIME_DMY_PATTERN).expect("datetime_dmy_pattern should be valid")
|
||||||
|
});
|
||||||
const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
||||||
^
|
^
|
||||||
['"]? # optional quotes
|
['"]? # optional quotes
|
||||||
@ -293,8 +298,9 @@ const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
|||||||
['"]? # optional quotes
|
['"]? # optional quotes
|
||||||
$
|
$
|
||||||
"#;
|
"#;
|
||||||
static DATETIME_YMD_RE: Lazy<Regex> =
|
static DATETIME_YMD_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Lazy::new(|| Regex::new(DATETIME_YMD_PATTERN).expect("datetime_ymd_pattern should be valid"));
|
Regex::new(DATETIME_YMD_PATTERN).expect("datetime_ymd_pattern should be valid")
|
||||||
|
});
|
||||||
//2023-03-24 16:44:17.865147299 -05:00
|
//2023-03-24 16:44:17.865147299 -05:00
|
||||||
const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
||||||
^
|
^
|
||||||
@ -327,23 +333,24 @@ const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
|||||||
['"]? # optional quotes
|
['"]? # optional quotes
|
||||||
$
|
$
|
||||||
"#;
|
"#;
|
||||||
static DATETIME_YMDZ_RE: Lazy<Regex> =
|
static DATETIME_YMDZ_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Lazy::new(|| Regex::new(DATETIME_YMDZ_PATTERN).expect("datetime_ymdz_pattern should be valid"));
|
Regex::new(DATETIME_YMDZ_PATTERN).expect("datetime_ymdz_pattern should be valid")
|
||||||
|
});
|
||||||
|
|
||||||
static FLOAT_RE: Lazy<Regex> = Lazy::new(|| {
|
static FLOAT_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Regex::new(r"^\s*[-+]?((\d*\.\d+)([eE][-+]?\d+)?|inf|NaN|(\d+)[eE][-+]?\d+|\d+\.)$")
|
Regex::new(r"^\s*[-+]?((\d*\.\d+)([eE][-+]?\d+)?|inf|NaN|(\d+)[eE][-+]?\d+|\d+\.)$")
|
||||||
.expect("float pattern should be valid")
|
.expect("float pattern should be valid")
|
||||||
});
|
});
|
||||||
|
|
||||||
static INTEGER_RE: Lazy<Regex> =
|
static INTEGER_RE: LazyLock<Regex> =
|
||||||
Lazy::new(|| Regex::new(r"^\s*-?(\d+)$").expect("integer pattern should be valid"));
|
LazyLock::new(|| Regex::new(r"^\s*-?(\d+)$").expect("integer pattern should be valid"));
|
||||||
|
|
||||||
static INTEGER_WITH_DELIMS_RE: Lazy<Regex> = Lazy::new(|| {
|
static INTEGER_WITH_DELIMS_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Regex::new(r"^\s*-?(\d{1,3}([,_]\d{3})+)$")
|
Regex::new(r"^\s*-?(\d{1,3}([,_]\d{3})+)$")
|
||||||
.expect("integer with delimiters pattern should be valid")
|
.expect("integer with delimiters pattern should be valid")
|
||||||
});
|
});
|
||||||
|
|
||||||
static BOOLEAN_RE: Lazy<Regex> = Lazy::new(|| {
|
static BOOLEAN_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
RegexBuilder::new(r"^\s*(true)$|^(false)$")
|
RegexBuilder::new(r"^\s*(true)$|^(false)$")
|
||||||
.case_insensitive(true)
|
.case_insensitive(true)
|
||||||
.build()
|
.build()
|
||||||
|
@ -271,7 +271,7 @@ pub fn debug_string_without_formatting(value: &Value) -> String {
|
|||||||
.join(" ")
|
.join(" ")
|
||||||
),
|
),
|
||||||
//TODO: It would be good to drill deeper into closures.
|
//TODO: It would be good to drill deeper into closures.
|
||||||
Value::Closure { val, .. } => format!("<Closure {}>", val.block_id),
|
Value::Closure { val, .. } => format!("<Closure {}>", val.block_id.get()),
|
||||||
Value::Nothing { .. } => String::new(),
|
Value::Nothing { .. } => String::new(),
|
||||||
Value::Error { error, .. } => format!("{error:?}"),
|
Value::Error { error, .. } => format!("{error:?}"),
|
||||||
Value::Binary { val, .. } => format!("{val:?}"),
|
Value::Binary { val, .. } => format!("{val:?}"),
|
||||||
|
@ -101,7 +101,7 @@ fn all_columns(span: Span) -> Value {
|
|||||||
let environment = {
|
let environment = {
|
||||||
let mut env_rec = Record::new();
|
let mut env_rec = Record::new();
|
||||||
for val in p.environ() {
|
for val in p.environ() {
|
||||||
if let Some((key, value)) = val.split_once('=') {
|
if let Some((key, value)) = val.to_string_lossy().split_once('=') {
|
||||||
let is_env_var_a_list = {
|
let is_env_var_a_list = {
|
||||||
{
|
{
|
||||||
#[cfg(target_family = "windows")]
|
#[cfg(target_family = "windows")]
|
||||||
@ -146,8 +146,8 @@ fn all_columns(span: Span) -> Value {
|
|||||||
"root" => root,
|
"root" => root,
|
||||||
"cwd" => cwd,
|
"cwd" => cwd,
|
||||||
"exe_path" => exe_path,
|
"exe_path" => exe_path,
|
||||||
"command" => Value::string(p.cmd().join(" "), span),
|
"command" => Value::string(p.cmd().join(std::ffi::OsStr::new(" ")).to_string_lossy(), span),
|
||||||
"name" => Value::string(p.name(), span),
|
"name" => Value::string(p.name().to_string_lossy(), span),
|
||||||
"environment" => environment,
|
"environment" => environment,
|
||||||
},
|
},
|
||||||
span,
|
span,
|
||||||
|
@ -152,7 +152,7 @@ fn truncate_data(
|
|||||||
let left_space = expected_width - width;
|
let left_space = expected_width - width;
|
||||||
let has_space_for_truncation_column = left_space > PAD;
|
let has_space_for_truncation_column = left_space > PAD;
|
||||||
if !has_space_for_truncation_column {
|
if !has_space_for_truncation_column {
|
||||||
peak_count -= 1;
|
peak_count = peak_count.saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_columns(data, peak_count);
|
remove_columns(data, peak_count);
|
||||||
@ -201,11 +201,18 @@ mod util {
|
|||||||
Value::Record { val: record, .. } => {
|
Value::Record { val: record, .. } => {
|
||||||
let (cols, vals): (Vec<_>, Vec<_>) = record.into_owned().into_iter().unzip();
|
let (cols, vals): (Vec<_>, Vec<_>) = record.into_owned().into_iter().unzip();
|
||||||
(
|
(
|
||||||
cols,
|
match cols.is_empty() {
|
||||||
vec![vals
|
true => vec![String::from("")],
|
||||||
|
false => cols,
|
||||||
|
},
|
||||||
|
match vals
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| debug_string_without_formatting(&s))
|
.map(|s| debug_string_without_formatting(&s))
|
||||||
.collect()],
|
.collect::<Vec<String>>()
|
||||||
|
{
|
||||||
|
vals if vals.is_empty() => vec![],
|
||||||
|
vals => vec![vals],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{BlockId, DeclId};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ViewIr;
|
pub struct ViewIr;
|
||||||
@ -86,7 +87,8 @@ the declaration may not be in scope.
|
|||||||
let decl_id = val
|
let decl_id = val
|
||||||
.try_into()
|
.try_into()
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|id| *id < engine_state.num_decls())
|
.map(DeclId::new)
|
||||||
|
.filter(|id| id.get() < engine_state.num_decls())
|
||||||
.ok_or_else(|| ShellError::IncorrectValue {
|
.ok_or_else(|| ShellError::IncorrectValue {
|
||||||
msg: "not a valid decl id".into(),
|
msg: "not a valid decl id".into(),
|
||||||
val_span: target.span(),
|
val_span: target.span(),
|
||||||
@ -102,11 +104,15 @@ the declaration may not be in scope.
|
|||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
// Block by ID - often shows up in IR
|
// Block by ID - often shows up in IR
|
||||||
Value::Int { val, .. } => val.try_into().map_err(|_| ShellError::IncorrectValue {
|
Value::Int { val, .. } => {
|
||||||
msg: "not a valid block id".into(),
|
val.try_into()
|
||||||
val_span: target.span(),
|
.map(BlockId::new)
|
||||||
call_span: call.head,
|
.map_err(|_| ShellError::IncorrectValue {
|
||||||
})?,
|
msg: "not a valid block id".into(),
|
||||||
|
val_span: target.span(),
|
||||||
|
call_span: call.head,
|
||||||
|
})?
|
||||||
|
}
|
||||||
// Pass through errors
|
// Pass through errors
|
||||||
Value::Error { error, .. } => return Err(*error),
|
Value::Error { error, .. } => return Err(*error),
|
||||||
_ => {
|
_ => {
|
||||||
@ -119,7 +125,7 @@ the declaration may not be in scope.
|
|||||||
|
|
||||||
let Some(block) = engine_state.try_get_block(block_id) else {
|
let Some(block) = engine_state.try_get_block(block_id) else {
|
||||||
return Err(ShellError::GenericError {
|
return Err(ShellError::GenericError {
|
||||||
error: format!("Unknown block ID: {block_id}"),
|
error: format!("Unknown block ID: {}", block_id.get()),
|
||||||
msg: "ensure the block ID is correct and try again".into(),
|
msg: "ensure the block ID is correct and try again".into(),
|
||||||
span: Some(target.span()),
|
span: Some(target.span()),
|
||||||
help: None,
|
help: None,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::Config;
|
use nu_protocol::{Config, DataSource, PipelineMetadata};
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ impl Command for ViewSource {
|
|||||||
let arg: Value = call.req(engine_state, stack, 0)?;
|
let arg: Value = call.req(engine_state, stack, 0)?;
|
||||||
let arg_span = arg.span();
|
let arg_span = arg.span();
|
||||||
|
|
||||||
match arg {
|
let source = match arg {
|
||||||
Value::String { val, .. } => {
|
Value::String { val, .. } => {
|
||||||
if let Some(decl_id) = engine_state.find_decl(val.as_bytes(), &[]) {
|
if let Some(decl_id) = engine_state.find_decl(val.as_bytes(), &[]) {
|
||||||
// arg is a command
|
// arg is a command
|
||||||
@ -193,7 +193,13 @@ impl Command for ViewSource {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
source.map(|x| {
|
||||||
|
x.set_metadata(Some(PipelineMetadata {
|
||||||
|
data_source: DataSource::None,
|
||||||
|
content_type: Some("application/x-nuscript".into()),
|
||||||
|
}))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
@ -46,7 +46,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
First,
|
First,
|
||||||
Flatten,
|
Flatten,
|
||||||
Get,
|
Get,
|
||||||
Group,
|
|
||||||
GroupBy,
|
GroupBy,
|
||||||
Headers,
|
Headers,
|
||||||
Insert,
|
Insert,
|
||||||
@ -188,8 +187,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
EncodeBase32Hex,
|
EncodeBase32Hex,
|
||||||
DecodeBase64,
|
DecodeBase64,
|
||||||
EncodeBase64,
|
EncodeBase64,
|
||||||
DecodeBase64Old,
|
|
||||||
EncodeBase64Old,
|
|
||||||
DetectColumns,
|
DetectColumns,
|
||||||
Parse,
|
Parse,
|
||||||
Split,
|
Split,
|
||||||
@ -390,6 +387,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
HttpOptions,
|
HttpOptions,
|
||||||
Url,
|
Url,
|
||||||
UrlBuildQuery,
|
UrlBuildQuery,
|
||||||
|
UrlSplitQuery,
|
||||||
UrlDecode,
|
UrlDecode,
|
||||||
UrlEncode,
|
UrlEncode,
|
||||||
UrlJoin,
|
UrlJoin,
|
||||||
|
@ -45,13 +45,9 @@ impl Command for ConfigReset {
|
|||||||
let only_env = call.has_flag(engine_state, stack, "env")?;
|
let only_env = call.has_flag(engine_state, stack, "env")?;
|
||||||
let no_backup = call.has_flag(engine_state, stack, "without-backup")?;
|
let no_backup = call.has_flag(engine_state, stack, "without-backup")?;
|
||||||
let span = call.head;
|
let span = call.head;
|
||||||
let mut config_path = match nu_path::config_dir() {
|
let Some(config_path) = nu_path::nu_config_dir() else {
|
||||||
Some(path) => path,
|
return Err(ShellError::ConfigDirNotFound { span: None });
|
||||||
None => {
|
|
||||||
return Err(ShellError::ConfigDirNotFound { span: None });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
config_path.push("nushell");
|
|
||||||
if !only_env {
|
if !only_env {
|
||||||
let mut nu_config = config_path.clone();
|
let mut nu_config = config_path.clone();
|
||||||
nu_config.push("config.nu");
|
nu_config.push("config.nu");
|
||||||
|
5
crates/nu-command/src/env/source_env.rs
vendored
5
crates/nu-command/src/env/source_env.rs
vendored
@ -2,7 +2,7 @@ use nu_engine::{
|
|||||||
command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block_with_early_return,
|
command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block_with_early_return,
|
||||||
redirect_env,
|
redirect_env,
|
||||||
};
|
};
|
||||||
use nu_protocol::engine::CommandType;
|
use nu_protocol::{engine::CommandType, BlockId};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Source a file for environment variables.
|
/// Source a file for environment variables.
|
||||||
@ -50,6 +50,7 @@ impl Command for SourceEnv {
|
|||||||
// Note: this hidden positional is the block_id that corresponded to the 0th position
|
// Note: this hidden positional is the block_id that corresponded to the 0th position
|
||||||
// it is put here by the parser
|
// it is put here by the parser
|
||||||
let block_id: i64 = call.req_parser_info(engine_state, caller_stack, "block_id")?;
|
let block_id: i64 = call.req_parser_info(engine_state, caller_stack, "block_id")?;
|
||||||
|
let block_id = BlockId::new(block_id as usize);
|
||||||
|
|
||||||
// Set the currently evaluated directory (file-relative PWD)
|
// Set the currently evaluated directory (file-relative PWD)
|
||||||
let file_path = if let Some(path) = find_in_dirs_env(
|
let file_path = if let Some(path) = find_in_dirs_env(
|
||||||
@ -78,7 +79,7 @@ impl Command for SourceEnv {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Evaluate the block
|
// Evaluate the block
|
||||||
let block = engine_state.get_block(block_id as usize).clone();
|
let block = engine_state.get_block(block_id).clone();
|
||||||
let mut callee_stack = caller_stack
|
let mut callee_stack = caller_stack
|
||||||
.gather_captures(engine_state, &block.captures)
|
.gather_captures(engine_state, &block.captures)
|
||||||
.reset_pipes();
|
.reset_pipes();
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use nu_cmd_base::util::get_init_cwd;
|
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_utils::filesystem::{have_permission, PermissionResult};
|
use nu_utils::filesystem::{have_permission, PermissionResult};
|
||||||
|
|
||||||
@ -41,12 +40,14 @@ impl Command for Cd {
|
|||||||
let physical = call.has_flag(engine_state, stack, "physical")?;
|
let physical = call.has_flag(engine_state, stack, "physical")?;
|
||||||
let path_val: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
|
let path_val: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
|
||||||
|
|
||||||
// If getting PWD failed, default to the initial directory. This way, the
|
// If getting PWD failed, default to the home directory. The user can
|
||||||
// user can use `cd` to recover PWD to a good state.
|
// use `cd` to reset PWD to a good state.
|
||||||
let cwd = engine_state
|
let cwd = engine_state
|
||||||
.cwd(Some(stack))
|
.cwd(Some(stack))
|
||||||
.ok()
|
.ok()
|
||||||
.unwrap_or_else(get_init_cwd);
|
.or_else(nu_path::home_dir)
|
||||||
|
.map(|path| path.into_std_path_buf())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let path_val = {
|
let path_val = {
|
||||||
if let Some(path) = path_val {
|
if let Some(path) = path_val {
|
||||||
@ -65,7 +66,7 @@ impl Command for Cd {
|
|||||||
if let Some(oldpwd) = stack.get_env_var(engine_state, "OLDPWD") {
|
if let Some(oldpwd) = stack.get_env_var(engine_state, "OLDPWD") {
|
||||||
oldpwd.to_path()?
|
oldpwd.to_path()?
|
||||||
} else {
|
} else {
|
||||||
cwd.into()
|
cwd
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Trim whitespace from the end of path.
|
// Trim whitespace from the end of path.
|
||||||
@ -106,7 +107,7 @@ impl Command for Cd {
|
|||||||
// Set OLDPWD.
|
// Set OLDPWD.
|
||||||
// We're using `Stack::get_env_var()` instead of `EngineState::cwd()` to avoid a conversion roundtrip.
|
// We're using `Stack::get_env_var()` instead of `EngineState::cwd()` to avoid a conversion roundtrip.
|
||||||
if let Some(oldpwd) = stack.get_env_var(engine_state, "PWD") {
|
if let Some(oldpwd) = stack.get_env_var(engine_state, "PWD") {
|
||||||
stack.add_env_var("OLDPWD".into(), oldpwd)
|
stack.add_env_var("OLDPWD".into(), oldpwd.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
match have_permission(&path) {
|
match have_permission(&path) {
|
||||||
|
@ -169,10 +169,16 @@ impl Command for Glob {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// below we have to check / instead of MAIN_SEPARATOR because glob uses / as separator
|
||||||
|
// using a glob like **\*.rs should fail because it's not a valid glob pattern
|
||||||
let folder_depth = if let Some(depth) = depth {
|
let folder_depth = if let Some(depth) = depth {
|
||||||
depth
|
depth
|
||||||
} else {
|
} else if glob_pattern.contains("**") {
|
||||||
usize::MAX
|
usize::MAX
|
||||||
|
} else if glob_pattern.contains('/') {
|
||||||
|
glob_pattern.split('/').count() + 1
|
||||||
|
} else {
|
||||||
|
1
|
||||||
};
|
};
|
||||||
|
|
||||||
let (prefix, glob) = match WaxGlob::new(&glob_pattern) {
|
let (prefix, glob) = match WaxGlob::new(&glob_pattern) {
|
||||||
|
@ -8,11 +8,14 @@ use nu_glob::MatchOptions;
|
|||||||
use nu_path::{expand_path_with, expand_to_real_path};
|
use nu_path::{expand_path_with, expand_to_real_path};
|
||||||
use nu_protocol::{DataSource, NuGlob, PipelineMetadata, Signals};
|
use nu_protocol::{DataSource, NuGlob, PipelineMetadata, Signals};
|
||||||
use pathdiff::diff_paths;
|
use pathdiff::diff_paths;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
sync::mpsc,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,6 +31,7 @@ struct Args {
|
|||||||
du: bool,
|
du: bool,
|
||||||
directory: bool,
|
directory: bool,
|
||||||
use_mime_type: bool,
|
use_mime_type: bool,
|
||||||
|
use_threads: bool,
|
||||||
call_span: Span,
|
call_span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +79,7 @@ impl Command for Ls {
|
|||||||
Some('D'),
|
Some('D'),
|
||||||
)
|
)
|
||||||
.switch("mime-type", "Show mime-type in type column instead of 'file' (based on filenames only; files' contents are not examined)", Some('m'))
|
.switch("mime-type", "Show mime-type in type column instead of 'file' (based on filenames only; files' contents are not examined)", Some('m'))
|
||||||
|
.switch("threads", "Use multiple threads to list contents. Output will be non-deterministic.", Some('t'))
|
||||||
.category(Category::FileSystem)
|
.category(Category::FileSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +97,7 @@ impl Command for Ls {
|
|||||||
let du = call.has_flag(engine_state, stack, "du")?;
|
let du = call.has_flag(engine_state, stack, "du")?;
|
||||||
let directory = call.has_flag(engine_state, stack, "directory")?;
|
let directory = call.has_flag(engine_state, stack, "directory")?;
|
||||||
let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?;
|
let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?;
|
||||||
|
let use_threads = call.has_flag(engine_state, stack, "threads")?;
|
||||||
let call_span = call.head;
|
let call_span = call.head;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
@ -104,6 +110,7 @@ impl Command for Ls {
|
|||||||
du,
|
du,
|
||||||
directory,
|
directory,
|
||||||
use_mime_type,
|
use_mime_type,
|
||||||
|
use_threads,
|
||||||
call_span,
|
call_span,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,22 +121,24 @@ impl Command for Ls {
|
|||||||
Some(pattern_arg)
|
Some(pattern_arg)
|
||||||
};
|
};
|
||||||
match input_pattern_arg {
|
match input_pattern_arg {
|
||||||
None => Ok(ls_for_one_pattern(None, args, engine_state.signals(), cwd)?
|
None => Ok(
|
||||||
.into_pipeline_data_with_metadata(
|
ls_for_one_pattern(None, args, engine_state.signals().clone(), cwd)?
|
||||||
call_span,
|
.into_pipeline_data_with_metadata(
|
||||||
engine_state.signals().clone(),
|
call_span,
|
||||||
PipelineMetadata {
|
engine_state.signals().clone(),
|
||||||
data_source: DataSource::Ls,
|
PipelineMetadata {
|
||||||
content_type: None,
|
data_source: DataSource::Ls,
|
||||||
},
|
content_type: None,
|
||||||
)),
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
Some(pattern) => {
|
Some(pattern) => {
|
||||||
let mut result_iters = vec![];
|
let mut result_iters = vec![];
|
||||||
for pat in pattern {
|
for pat in pattern {
|
||||||
result_iters.push(ls_for_one_pattern(
|
result_iters.push(ls_for_one_pattern(
|
||||||
Some(pat),
|
Some(pat),
|
||||||
args,
|
args,
|
||||||
engine_state.signals(),
|
engine_state.signals().clone(),
|
||||||
cwd.clone(),
|
cwd.clone(),
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
@ -213,9 +222,27 @@ impl Command for Ls {
|
|||||||
fn ls_for_one_pattern(
|
fn ls_for_one_pattern(
|
||||||
pattern_arg: Option<Spanned<NuGlob>>,
|
pattern_arg: Option<Spanned<NuGlob>>,
|
||||||
args: Args,
|
args: Args,
|
||||||
signals: &Signals,
|
signals: Signals,
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
) -> Result<Box<dyn Iterator<Item = Value> + Send>, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
fn create_pool(num_threads: usize) -> Result<rayon::ThreadPool, ShellError> {
|
||||||
|
match rayon::ThreadPoolBuilder::new()
|
||||||
|
.num_threads(num_threads)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Err(e) => Err(e).map_err(|e| ShellError::GenericError {
|
||||||
|
error: "Error creating thread pool".into(),
|
||||||
|
msg: e.to_string(),
|
||||||
|
span: Some(Span::unknown()),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
}),
|
||||||
|
Ok(pool) => Ok(pool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
let Args {
|
let Args {
|
||||||
all,
|
all,
|
||||||
long,
|
long,
|
||||||
@ -224,6 +251,7 @@ fn ls_for_one_pattern(
|
|||||||
du,
|
du,
|
||||||
directory,
|
directory,
|
||||||
use_mime_type,
|
use_mime_type,
|
||||||
|
use_threads,
|
||||||
call_span,
|
call_span,
|
||||||
} = args;
|
} = args;
|
||||||
let pattern_arg = {
|
let pattern_arg = {
|
||||||
@ -281,7 +309,7 @@ fn ls_for_one_pattern(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if is_empty_dir(&tmp_expanded) {
|
if is_empty_dir(&tmp_expanded) {
|
||||||
return Ok(Box::new(vec![].into_iter()));
|
return Ok(Value::test_nothing().into_pipeline_data());
|
||||||
}
|
}
|
||||||
just_read_dir = !(pat.item.is_expand() && pat.item.as_ref().contains(GLOB_CHARS));
|
just_read_dir = !(pat.item.is_expand() && pat.item.as_ref().contains(GLOB_CHARS));
|
||||||
}
|
}
|
||||||
@ -300,7 +328,7 @@ fn ls_for_one_pattern(
|
|||||||
if directory {
|
if directory {
|
||||||
(NuGlob::Expand(".".to_string()), false)
|
(NuGlob::Expand(".".to_string()), false)
|
||||||
} else if is_empty_dir(&cwd) {
|
} else if is_empty_dir(&cwd) {
|
||||||
return Ok(Box::new(vec![].into_iter()));
|
return Ok(Value::test_nothing().into_pipeline_data());
|
||||||
} else {
|
} else {
|
||||||
(NuGlob::Expand("*".to_string()), false)
|
(NuGlob::Expand("*".to_string()), false)
|
||||||
}
|
}
|
||||||
@ -338,92 +366,130 @@ fn ls_for_one_pattern(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hidden_dirs = vec![];
|
let hidden_dirs = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
let signals = signals.clone();
|
let signals_clone = signals.clone();
|
||||||
Ok(Box::new(paths_peek.filter_map(move |x| match x {
|
|
||||||
Ok(path) => {
|
|
||||||
let metadata = match std::fs::symlink_metadata(&path) {
|
|
||||||
Ok(metadata) => Some(metadata),
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
if path_contains_hidden_folder(&path, &hidden_dirs) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !all && !hidden_dir_specified && is_hidden_dir(&path) {
|
let pool = if use_threads {
|
||||||
if path.is_dir() {
|
let count = std::thread::available_parallelism()?.get();
|
||||||
hidden_dirs.push(path);
|
create_pool(count)?
|
||||||
}
|
} else {
|
||||||
return None;
|
create_pool(1)?
|
||||||
}
|
};
|
||||||
|
|
||||||
let display_name = if short_names {
|
pool.install(|| {
|
||||||
path.file_name().map(|os| os.to_string_lossy().to_string())
|
paths_peek
|
||||||
} else if full_paths || absolute_path {
|
.par_bridge()
|
||||||
Some(path.to_string_lossy().to_string())
|
.filter_map(move |x| match x {
|
||||||
} else if let Some(prefix) = &prefix {
|
Ok(path) => {
|
||||||
if let Ok(remainder) = path.strip_prefix(prefix) {
|
let metadata = match std::fs::symlink_metadata(&path) {
|
||||||
if directory {
|
Ok(metadata) => Some(metadata),
|
||||||
// When the path is the same as the cwd, path_diff should be "."
|
Err(_) => None,
|
||||||
let path_diff = if let Some(path_diff_not_dot) = diff_paths(&path, &cwd) {
|
};
|
||||||
let path_diff_not_dot = path_diff_not_dot.to_string_lossy();
|
let hidden_dir_clone = Arc::clone(&hidden_dirs);
|
||||||
if path_diff_not_dot.is_empty() {
|
let mut hidden_dir_mutex = hidden_dir_clone
|
||||||
".".to_string()
|
.lock()
|
||||||
|
.expect("Unable to acquire lock for hidden_dirs");
|
||||||
|
if path_contains_hidden_folder(&path, &hidden_dir_mutex) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !all && !hidden_dir_specified && is_hidden_dir(&path) {
|
||||||
|
if path.is_dir() {
|
||||||
|
hidden_dir_mutex.push(path);
|
||||||
|
drop(hidden_dir_mutex);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let display_name = if short_names {
|
||||||
|
path.file_name().map(|os| os.to_string_lossy().to_string())
|
||||||
|
} else if full_paths || absolute_path {
|
||||||
|
Some(path.to_string_lossy().to_string())
|
||||||
|
} else if let Some(prefix) = &prefix {
|
||||||
|
if let Ok(remainder) = path.strip_prefix(prefix) {
|
||||||
|
if directory {
|
||||||
|
// When the path is the same as the cwd, path_diff should be "."
|
||||||
|
let path_diff =
|
||||||
|
if let Some(path_diff_not_dot) = diff_paths(&path, &cwd) {
|
||||||
|
let path_diff_not_dot = path_diff_not_dot.to_string_lossy();
|
||||||
|
if path_diff_not_dot.is_empty() {
|
||||||
|
".".to_string()
|
||||||
|
} else {
|
||||||
|
path_diff_not_dot.to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path.to_string_lossy().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(path_diff)
|
||||||
} else {
|
} else {
|
||||||
path_diff_not_dot.to_string()
|
let new_prefix = if let Some(pfx) = diff_paths(prefix, &cwd) {
|
||||||
|
pfx
|
||||||
|
} else {
|
||||||
|
prefix.to_path_buf()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(new_prefix.join(remainder).to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
path.to_string_lossy().to_string()
|
Some(path.to_string_lossy().to_string())
|
||||||
};
|
}
|
||||||
|
|
||||||
Some(path_diff)
|
|
||||||
} else {
|
} else {
|
||||||
let new_prefix = if let Some(pfx) = diff_paths(prefix, &cwd) {
|
Some(path.to_string_lossy().to_string())
|
||||||
pfx
|
|
||||||
} else {
|
|
||||||
prefix.to_path_buf()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(new_prefix.join(remainder).to_string_lossy().to_string())
|
|
||||||
}
|
}
|
||||||
} else {
|
.ok_or_else(|| ShellError::GenericError {
|
||||||
Some(path.to_string_lossy().to_string())
|
error: format!("Invalid file name: {:}", path.to_string_lossy()),
|
||||||
}
|
msg: "invalid file name".into(),
|
||||||
} else {
|
span: Some(call_span),
|
||||||
Some(path.to_string_lossy().to_string())
|
help: None,
|
||||||
}
|
inner: vec![],
|
||||||
.ok_or_else(|| ShellError::GenericError {
|
});
|
||||||
error: format!("Invalid file name: {:}", path.to_string_lossy()),
|
|
||||||
msg: "invalid file name".into(),
|
|
||||||
span: Some(call_span),
|
|
||||||
help: None,
|
|
||||||
inner: vec![],
|
|
||||||
});
|
|
||||||
|
|
||||||
match display_name {
|
match display_name {
|
||||||
Ok(name) => {
|
Ok(name) => {
|
||||||
let entry = dir_entry_dict(
|
let entry = dir_entry_dict(
|
||||||
&path,
|
&path,
|
||||||
&name,
|
&name,
|
||||||
metadata.as_ref(),
|
metadata.as_ref(),
|
||||||
call_span,
|
call_span,
|
||||||
long,
|
long,
|
||||||
du,
|
du,
|
||||||
&signals,
|
&signals_clone,
|
||||||
use_mime_type,
|
use_mime_type,
|
||||||
args.full_paths,
|
args.full_paths,
|
||||||
);
|
);
|
||||||
match entry {
|
match entry {
|
||||||
Ok(value) => Some(value),
|
Ok(value) => Some(value),
|
||||||
|
Err(err) => Some(Value::error(err, call_span)),
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(err) => Some(Value::error(err, call_span)),
|
Err(err) => Some(Value::error(err, call_span)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Some(Value::error(err, call_span)),
|
Err(err) => Some(Value::error(err, call_span)),
|
||||||
}
|
})
|
||||||
}
|
.try_for_each(|stream| {
|
||||||
Err(err) => Some(Value::error(err, call_span)),
|
tx.send(stream).map_err(|e| ShellError::GenericError {
|
||||||
})))
|
error: "Error streaming data".into(),
|
||||||
|
msg: e.to_string(),
|
||||||
|
span: Some(call_span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|err| ShellError::GenericError {
|
||||||
|
error: "Unable to create a rayon pool".into(),
|
||||||
|
msg: err.to_string(),
|
||||||
|
span: Some(call_span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(rx
|
||||||
|
.into_iter()
|
||||||
|
.into_pipeline_data(call_span, signals.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn permission_denied(dir: impl AsRef<Path>) -> bool {
|
fn permission_denied(dir: impl AsRef<Path>) -> bool {
|
||||||
|
@ -146,6 +146,9 @@ impl Command for Open {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Assigning content type should only happen in raw mode. Otherwise, the content
|
||||||
|
// will potentially be in one of the built-in nushell `from xxx` formats and therefore
|
||||||
|
// cease to be in the original content-type.... or so I'm told. :)
|
||||||
let content_type = if raw {
|
let content_type = if raw {
|
||||||
path.extension()
|
path.extension()
|
||||||
.map(|ext| ext.to_string_lossy().to_string())
|
.map(|ext| ext.to_string_lossy().to_string())
|
||||||
@ -283,6 +286,9 @@ fn detect_content_type(extension: &str) -> Option<String> {
|
|||||||
match extension {
|
match extension {
|
||||||
// Per RFC-9512, application/yaml should be used
|
// Per RFC-9512, application/yaml should be used
|
||||||
"yaml" | "yml" => Some("application/yaml".to_string()),
|
"yaml" | "yml" => Some("application/yaml".to_string()),
|
||||||
|
"nu" => Some("application/x-nuscript".to_string()),
|
||||||
|
"json" | "jsonl" | "ndjson" => Some("application/json".to_string()),
|
||||||
|
"nuon" => Some("application/x-nuon".to_string()),
|
||||||
_ => mime_guess::from_ext(extension)
|
_ => mime_guess::from_ext(extension)
|
||||||
.first()
|
.first()
|
||||||
.map(|mime| mime.to_string()),
|
.map(|mime| mime.to_string()),
|
||||||
|
@ -12,6 +12,7 @@ use std::{
|
|||||||
io::{self, BufRead, BufReader, Read, Write},
|
io::{self, BufRead, BufReader, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
thread,
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -182,6 +183,8 @@ impl Command for Save {
|
|||||||
}
|
}
|
||||||
(None, None) => {}
|
(None, None) => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
child.wait()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +273,7 @@ impl Command for Save {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
|
fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
|
||||||
(Some(OutDest::Capture), Some(OutDest::Capture))
|
(Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,7 +483,7 @@ fn stream_to_file(
|
|||||||
|
|
||||||
let mut bar = progress_bar::NuProgressBar::new(known_size);
|
let mut bar = progress_bar::NuProgressBar::new(known_size);
|
||||||
|
|
||||||
// TODO: reduce the number of progress bar updates?
|
let mut last_update = Instant::now();
|
||||||
|
|
||||||
let mut reader = BufReader::new(source);
|
let mut reader = BufReader::new(source);
|
||||||
|
|
||||||
@ -497,7 +500,10 @@ fn stream_to_file(
|
|||||||
let len = buf.len();
|
let len = buf.len();
|
||||||
reader.consume(len);
|
reader.consume(len);
|
||||||
bytes_processed += len as u64;
|
bytes_processed += len as u64;
|
||||||
bar.update_bar(bytes_processed);
|
if last_update.elapsed() >= Duration::from_millis(75) {
|
||||||
|
bar.update_bar(bytes_processed);
|
||||||
|
last_update = Instant::now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||||
Err(e) => break Err(e),
|
Err(e) => break Err(e),
|
||||||
|
@ -58,9 +58,8 @@ impl Command for Start {
|
|||||||
open_path(url.as_str(), engine_state, stack, path.span)?;
|
open_path(url.as_str(), engine_state, stack, path.span)?;
|
||||||
} else {
|
} else {
|
||||||
// try to distinguish between file not found and opening url without prefix
|
// try to distinguish between file not found and opening url without prefix
|
||||||
if let Ok(canon_path) =
|
let cwd = engine_state.cwd(Some(stack))?;
|
||||||
canonicalize_with(path_no_whitespace, std::env::current_dir()?.as_path())
|
if let Ok(canon_path) = canonicalize_with(path_no_whitespace, cwd) {
|
||||||
{
|
|
||||||
open_path(canon_path, engine_state, stack, path.span)?;
|
open_path(canon_path, engine_state, stack, path.span)?;
|
||||||
} else {
|
} else {
|
||||||
// open crate does not allow opening URL without prefix
|
// open crate does not allow opening URL without prefix
|
||||||
|
@ -48,6 +48,11 @@ impl Command for Touch {
|
|||||||
"do not create the file if it does not exist",
|
"do not create the file if it does not exist",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
.switch(
|
||||||
|
"no-deref",
|
||||||
|
"do not follow symlinks",
|
||||||
|
Some('s')
|
||||||
|
)
|
||||||
.category(Category::FileSystem)
|
.category(Category::FileSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +69,7 @@ impl Command for Touch {
|
|||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let mut change_mtime: bool = call.has_flag(engine_state, stack, "modified")?;
|
let mut change_mtime: bool = call.has_flag(engine_state, stack, "modified")?;
|
||||||
let mut change_atime: bool = call.has_flag(engine_state, stack, "access")?;
|
let mut change_atime: bool = call.has_flag(engine_state, stack, "access")?;
|
||||||
|
let no_follow_symlinks: bool = call.has_flag(engine_state, stack, "no-deref")?;
|
||||||
let reference: Option<Spanned<String>> = call.get_flag(engine_state, stack, "reference")?;
|
let reference: Option<Spanned<String>> = call.get_flag(engine_state, stack, "reference")?;
|
||||||
let no_create: bool = call.has_flag(engine_state, stack, "no-create")?;
|
let no_create: bool = call.has_flag(engine_state, stack, "no-create")?;
|
||||||
let files: Vec<Spanned<NuGlob>> = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
let files: Vec<Spanned<NuGlob>> = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
||||||
@ -88,19 +94,29 @@ impl Command for Touch {
|
|||||||
|
|
||||||
if let Some(reference) = reference {
|
if let Some(reference) = reference {
|
||||||
let reference_path = nu_path::expand_path_with(reference.item, &cwd, true);
|
let reference_path = nu_path::expand_path_with(reference.item, &cwd, true);
|
||||||
if !reference_path.exists() {
|
let exists = if no_follow_symlinks {
|
||||||
|
// There's no symlink_exists function, so we settle for
|
||||||
|
// getting direct metadata and if it's OK, it exists
|
||||||
|
reference_path.symlink_metadata().is_ok()
|
||||||
|
} else {
|
||||||
|
reference_path.exists()
|
||||||
|
};
|
||||||
|
if !exists {
|
||||||
return Err(ShellError::FileNotFoundCustom {
|
return Err(ShellError::FileNotFoundCustom {
|
||||||
msg: "Reference path not found".into(),
|
msg: "Reference path not found".into(),
|
||||||
span: reference.span,
|
span: reference.span,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = reference_path
|
let metadata = if no_follow_symlinks {
|
||||||
.metadata()
|
reference_path.symlink_metadata()
|
||||||
.map_err(|err| ShellError::IOErrorSpanned {
|
} else {
|
||||||
msg: format!("Failed to read metadata: {err}"),
|
reference_path.metadata()
|
||||||
span: reference.span,
|
};
|
||||||
})?;
|
let metadata = metadata.map_err(|err| ShellError::IOErrorSpanned {
|
||||||
|
msg: format!("Failed to read metadata: {err}"),
|
||||||
|
span: reference.span,
|
||||||
|
})?;
|
||||||
mtime = metadata
|
mtime = metadata
|
||||||
.modified()
|
.modified()
|
||||||
.map_err(|err| ShellError::IOErrorSpanned {
|
.map_err(|err| ShellError::IOErrorSpanned {
|
||||||
@ -117,14 +133,27 @@ impl Command for Touch {
|
|||||||
|
|
||||||
for glob in files {
|
for glob in files {
|
||||||
let path = expand_path_with(glob.item.as_ref(), &cwd, glob.item.is_expand());
|
let path = expand_path_with(glob.item.as_ref(), &cwd, glob.item.is_expand());
|
||||||
|
let exists = if no_follow_symlinks {
|
||||||
|
path.symlink_metadata().is_ok()
|
||||||
|
} else {
|
||||||
|
path.exists()
|
||||||
|
};
|
||||||
|
|
||||||
// If --no-create is passed and the file/dir does not exist there's nothing to do
|
// If --no-create is passed and the file/dir does not exist there's nothing to do
|
||||||
if no_create && !path.exists() {
|
if no_create && !exists {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a file at the given path unless the path is a directory
|
// If --no-deref was passed in, the behavior of touch is to error on missing
|
||||||
if !path.is_dir() {
|
if no_follow_symlinks && !exists {
|
||||||
|
return Err(ShellError::FileNotFound {
|
||||||
|
file: path.to_string_lossy().into_owned(),
|
||||||
|
span: glob.span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a file at the given path unless the path is a directory (or a symlink with -d)
|
||||||
|
if !path.is_dir() && (!no_follow_symlinks || !path.is_symlink()) {
|
||||||
if let Err(err) = OpenOptions::new()
|
if let Err(err) = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
@ -138,9 +167,31 @@ impl Command for Touch {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We have to inefficiently access the target metadata to not reset it
|
||||||
|
// in set_symlink_file_times, because the filetime doesn't expose individual methods for it
|
||||||
|
let get_target_metadata = || {
|
||||||
|
path.symlink_metadata()
|
||||||
|
.map_err(|err| ShellError::IOErrorSpanned {
|
||||||
|
msg: format!("Failed to read metadata: {err}"),
|
||||||
|
span: glob.span,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
if change_mtime {
|
if change_mtime {
|
||||||
if let Err(err) = filetime::set_file_mtime(&path, FileTime::from_system_time(mtime))
|
let result = if no_follow_symlinks {
|
||||||
{
|
filetime::set_symlink_file_times(
|
||||||
|
&path,
|
||||||
|
if change_atime {
|
||||||
|
FileTime::from_system_time(atime)
|
||||||
|
} else {
|
||||||
|
FileTime::from_system_time(get_target_metadata()?.accessed()?)
|
||||||
|
},
|
||||||
|
FileTime::from_system_time(mtime),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
filetime::set_file_mtime(&path, FileTime::from_system_time(mtime))
|
||||||
|
};
|
||||||
|
if let Err(err) = result {
|
||||||
return Err(ShellError::ChangeModifiedTimeNotPossible {
|
return Err(ShellError::ChangeModifiedTimeNotPossible {
|
||||||
msg: format!("Failed to change the modified time: {err}"),
|
msg: format!("Failed to change the modified time: {err}"),
|
||||||
span: glob.span,
|
span: glob.span,
|
||||||
@ -149,8 +200,20 @@ impl Command for Touch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if change_atime {
|
if change_atime {
|
||||||
if let Err(err) = filetime::set_file_atime(&path, FileTime::from_system_time(atime))
|
let result = if no_follow_symlinks {
|
||||||
{
|
filetime::set_symlink_file_times(
|
||||||
|
&path,
|
||||||
|
FileTime::from_system_time(atime),
|
||||||
|
if change_mtime {
|
||||||
|
FileTime::from_system_time(mtime)
|
||||||
|
} else {
|
||||||
|
FileTime::from_system_time(get_target_metadata()?.modified()?)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
filetime::set_file_atime(&path, FileTime::from_system_time(atime))
|
||||||
|
};
|
||||||
|
if let Err(err) = result {
|
||||||
return Err(ShellError::ChangeAccessTimeNotPossible {
|
return Err(ShellError::ChangeAccessTimeNotPossible {
|
||||||
msg: format!("Failed to change the access time: {err}"),
|
msg: format!("Failed to change the access time: {err}"),
|
||||||
span: glob.span,
|
span: glob.span,
|
||||||
|
@ -157,7 +157,7 @@ fn flat_value(columns: &[CellPath], item: Value, all: bool) -> Vec<Value> {
|
|||||||
let mut inner_table = None;
|
let mut inner_table = None;
|
||||||
|
|
||||||
for (column_index, (column, value)) in val.into_owned().into_iter().enumerate() {
|
for (column_index, (column, value)) in val.into_owned().into_iter().enumerate() {
|
||||||
let column_requested = columns.iter().find(|c| c.to_string() == column);
|
let column_requested = columns.iter().find(|c| c.to_column_name() == column);
|
||||||
let need_flatten = { columns.is_empty() || column_requested.is_some() };
|
let need_flatten = { columns.is_empty() || column_requested.is_some() };
|
||||||
let span = value.span();
|
let span = value.span();
|
||||||
|
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
use nu_engine::command_prelude::*;
|
|
||||||
use nu_protocol::{report_shell_warning, ValueIterator};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Group;
|
|
||||||
|
|
||||||
impl Command for Group {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"group"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signature(&self) -> Signature {
|
|
||||||
Signature::build("group")
|
|
||||||
// TODO: It accepts Table also, but currently there is no Table
|
|
||||||
// example. Perhaps Table should be a subtype of List, in which case
|
|
||||||
// the current signature would suffice even when a Table example
|
|
||||||
// exists.
|
|
||||||
.input_output_types(vec![(
|
|
||||||
Type::List(Box::new(Type::Any)),
|
|
||||||
Type::List(Box::new(Type::List(Box::new(Type::Any)))),
|
|
||||||
)])
|
|
||||||
.required("group_size", SyntaxShape::Int, "The size of each group.")
|
|
||||||
.category(Category::Filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
|
||||||
"Groups input into groups of `group_size`."
|
|
||||||
}
|
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
|
||||||
let stream_test_1 = vec![
|
|
||||||
Value::list(
|
|
||||||
vec![Value::test_int(1), Value::test_int(2)],
|
|
||||||
Span::test_data(),
|
|
||||||
),
|
|
||||||
Value::list(
|
|
||||||
vec![Value::test_int(3), Value::test_int(4)],
|
|
||||||
Span::test_data(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
vec![Example {
|
|
||||||
example: "[1 2 3 4] | group 2",
|
|
||||||
description: "Group the a list by pairs",
|
|
||||||
result: Some(Value::list(stream_test_1, Span::test_data())),
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
&self,
|
|
||||||
engine_state: &EngineState,
|
|
||||||
stack: &mut Stack,
|
|
||||||
call: &Call,
|
|
||||||
input: PipelineData,
|
|
||||||
) -> Result<PipelineData, ShellError> {
|
|
||||||
let head = call.head;
|
|
||||||
|
|
||||||
report_shell_warning(
|
|
||||||
engine_state,
|
|
||||||
&ShellError::Deprecated {
|
|
||||||
old_command: "group".into(),
|
|
||||||
new_suggestion: "the new `chunks` command".into(),
|
|
||||||
span: head,
|
|
||||||
url: "`help chunks`".into(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let group_size: Spanned<usize> = call.req(engine_state, stack, 0)?;
|
|
||||||
let metadata = input.metadata();
|
|
||||||
|
|
||||||
let each_group_iterator = EachGroupIterator {
|
|
||||||
group_size: group_size.item,
|
|
||||||
input: Box::new(input.into_iter()),
|
|
||||||
span: head,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(each_group_iterator.into_pipeline_data_with_metadata(
|
|
||||||
head,
|
|
||||||
engine_state.signals().clone(),
|
|
||||||
metadata,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EachGroupIterator {
|
|
||||||
group_size: usize,
|
|
||||||
input: ValueIterator,
|
|
||||||
span: Span,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Iterator for EachGroupIterator {
|
|
||||||
type Item = Value;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let mut group = vec![];
|
|
||||||
let mut current_count = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let item = self.input.next();
|
|
||||||
|
|
||||||
match item {
|
|
||||||
Some(v) => {
|
|
||||||
group.push(v);
|
|
||||||
|
|
||||||
current_count += 1;
|
|
||||||
if current_count >= self.group_size {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if group.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Value::list(group, self.span))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_examples() {
|
|
||||||
use crate::test_examples;
|
|
||||||
|
|
||||||
test_examples(Group {})
|
|
||||||
}
|
|
||||||
}
|
|
@ -38,6 +38,14 @@ impl Command for GroupBy {
|
|||||||
"Splits a list or table into groups, and returns a record containing those groups."
|
"Splits a list or table into groups, and returns a record containing those groups."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"the group-by command makes some assumptions:
|
||||||
|
- if the input data is not a string, the grouper will convert the key to string but the values will remain in their original format. e.g. with bools, "true" and true would be in the same group (see example).
|
||||||
|
- datetime is formatted based on your configuration setting. use `format date` to change the format.
|
||||||
|
- filesize is formatted based on your configuration setting. use `format filesize` to change the format.
|
||||||
|
- some nushell values are not supported, such as closures."#
|
||||||
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
&self,
|
&self,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
@ -114,6 +122,20 @@ impl Command for GroupBy {
|
|||||||
}),
|
}),
|
||||||
])),
|
])),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Group bools, whether they are strings or actual bools",
|
||||||
|
example: r#"[true "true" false "false"] | group-by"#,
|
||||||
|
result: Some(Value::test_record(record! {
|
||||||
|
"true" => Value::test_list(vec![
|
||||||
|
Value::test_bool(true),
|
||||||
|
Value::test_string("true"),
|
||||||
|
]),
|
||||||
|
"false" => Value::test_list(vec![
|
||||||
|
Value::test_bool(false),
|
||||||
|
Value::test_string("false"),
|
||||||
|
]),
|
||||||
|
})),
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,6 +149,7 @@ pub fn group_by(
|
|||||||
let head = call.head;
|
let head = call.head;
|
||||||
let grouper: Option<Value> = call.opt(engine_state, stack, 0)?;
|
let grouper: Option<Value> = call.opt(engine_state, stack, 0)?;
|
||||||
let to_table = call.has_flag(engine_state, stack, "to-table")?;
|
let to_table = call.has_flag(engine_state, stack, "to-table")?;
|
||||||
|
let config = engine_state.get_config();
|
||||||
|
|
||||||
let values: Vec<Value> = input.into_iter().collect();
|
let values: Vec<Value> = input.into_iter().collect();
|
||||||
if values.is_empty() {
|
if values.is_empty() {
|
||||||
@ -137,7 +160,7 @@ pub fn group_by(
|
|||||||
Some(grouper) => {
|
Some(grouper) => {
|
||||||
let span = grouper.span();
|
let span = grouper.span();
|
||||||
match grouper {
|
match grouper {
|
||||||
Value::CellPath { val, .. } => group_cell_path(val, values)?,
|
Value::CellPath { val, .. } => group_cell_path(val, values, config)?,
|
||||||
Value::Closure { val, .. } => {
|
Value::Closure { val, .. } => {
|
||||||
group_closure(values, span, *val, engine_state, stack)?
|
group_closure(values, span, *val, engine_state, stack)?
|
||||||
}
|
}
|
||||||
@ -149,7 +172,7 @@ pub fn group_by(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => group_no_grouper(values)?,
|
None => group_no_grouper(values, config)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let value = if to_table {
|
let value = if to_table {
|
||||||
@ -164,6 +187,7 @@ pub fn group_by(
|
|||||||
fn group_cell_path(
|
fn group_cell_path(
|
||||||
column_name: CellPath,
|
column_name: CellPath,
|
||||||
values: Vec<Value>,
|
values: Vec<Value>,
|
||||||
|
config: &nu_protocol::Config,
|
||||||
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
|
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
|
||||||
let mut groups = IndexMap::<_, Vec<_>>::new();
|
let mut groups = IndexMap::<_, Vec<_>>::new();
|
||||||
|
|
||||||
@ -176,18 +200,21 @@ fn group_cell_path(
|
|||||||
continue; // likely the result of a failed optional access, ignore this value
|
continue; // likely the result of a failed optional access, ignore this value
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = key.coerce_string()?;
|
let key = key.to_abbreviated_string(config);
|
||||||
groups.entry(key).or_default().push(value);
|
groups.entry(key).or_default().push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(groups)
|
Ok(groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn group_no_grouper(values: Vec<Value>) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
|
fn group_no_grouper(
|
||||||
|
values: Vec<Value>,
|
||||||
|
config: &nu_protocol::Config,
|
||||||
|
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
|
||||||
let mut groups = IndexMap::<_, Vec<_>>::new();
|
let mut groups = IndexMap::<_, Vec<_>>::new();
|
||||||
|
|
||||||
for value in values.into_iter() {
|
for value in values.into_iter() {
|
||||||
let key = value.coerce_string()?;
|
let key = value.to_abbreviated_string(config);
|
||||||
groups.entry(key).or_default().push(value);
|
groups.entry(key).or_default().push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,12 +230,13 @@ fn group_closure(
|
|||||||
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
|
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
|
||||||
let mut groups = IndexMap::<_, Vec<_>>::new();
|
let mut groups = IndexMap::<_, Vec<_>>::new();
|
||||||
let mut closure = ClosureEval::new(engine_state, stack, closure);
|
let mut closure = ClosureEval::new(engine_state, stack, closure);
|
||||||
|
let config = engine_state.get_config();
|
||||||
|
|
||||||
for value in values {
|
for value in values {
|
||||||
let key = closure
|
let key = closure
|
||||||
.run_with_value(value.clone())?
|
.run_with_value(value.clone())?
|
||||||
.into_value(span)?
|
.into_value(span)?
|
||||||
.coerce_into_string()?;
|
.to_abbreviated_string(config);
|
||||||
|
|
||||||
groups.entry(key).or_default().push(value);
|
groups.entry(key).or_default().push(value);
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ impl Command for Join {
|
|||||||
Signature::build("join")
|
Signature::build("join")
|
||||||
.required(
|
.required(
|
||||||
"right-table",
|
"right-table",
|
||||||
SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
SyntaxShape::Table([].into()),
|
||||||
"The right table in the join.",
|
"The right table in the join.",
|
||||||
)
|
)
|
||||||
.required(
|
.required(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -9,12 +11,15 @@ impl Command for Length {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"Count the number of items in an input list or rows in a table."
|
"Count the number of items in an input list, rows in a table, or bytes in binary data."
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature(&self) -> nu_protocol::Signature {
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
Signature::build("length")
|
Signature::build("length")
|
||||||
.input_output_types(vec![(Type::List(Box::new(Type::Any)), Type::Int)])
|
.input_output_types(vec![
|
||||||
|
(Type::List(Box::new(Type::Any)), Type::Int),
|
||||||
|
(Type::Binary, Type::Int),
|
||||||
|
])
|
||||||
.category(Category::Filters)
|
.category(Category::Filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +49,11 @@ impl Command for Length {
|
|||||||
example: "[{a:1 b:2}, {a:2 b:3}] | length",
|
example: "[{a:1 b:2}, {a:2 b:3}] | length",
|
||||||
result: Some(Value::test_int(2)),
|
result: Some(Value::test_int(2)),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Count the number of bytes in binary data",
|
||||||
|
example: "0x[01 02] | length",
|
||||||
|
result: Some(Value::test_int(2)),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,6 +74,19 @@ fn length_row(call: &Call, input: PipelineData) -> Result<PipelineData, ShellErr
|
|||||||
src_span: span,
|
src_span: span,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
PipelineData::Value(Value::Binary { val, .. }, ..) => {
|
||||||
|
Ok(Value::int(val.len() as i64, call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
PipelineData::ByteStream(stream, _) if stream.type_().is_binary_coercible() => {
|
||||||
|
Ok(Value::int(
|
||||||
|
match stream.reader() {
|
||||||
|
Some(r) => r.bytes().count() as i64,
|
||||||
|
None => 0,
|
||||||
|
},
|
||||||
|
call.head,
|
||||||
|
)
|
||||||
|
.into_pipeline_data())
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let mut count: i64 = 0;
|
let mut count: i64 = 0;
|
||||||
// Check for and propagate errors
|
// Check for and propagate errors
|
||||||
|
@ -38,21 +38,21 @@ repeating this process with row 1, and so on."#
|
|||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
example: "[a b c] | wrap name | merge ( [1 2 3] | wrap index )",
|
example: "[a b c] | wrap name | merge ( [47 512 618] | wrap id )",
|
||||||
description: "Add an 'index' column to the input table",
|
description: "Add an 'id' column to the input table",
|
||||||
result: Some(Value::list(
|
result: Some(Value::list(
|
||||||
vec![
|
vec![
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"name" => Value::test_string("a"),
|
"name" => Value::test_string("a"),
|
||||||
"index" => Value::test_int(1),
|
"id" => Value::test_int(47),
|
||||||
}),
|
}),
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"name" => Value::test_string("b"),
|
"name" => Value::test_string("b"),
|
||||||
"index" => Value::test_int(2),
|
"id" => Value::test_int(512),
|
||||||
}),
|
}),
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"name" => Value::test_string("c"),
|
"name" => Value::test_string("c"),
|
||||||
"index" => Value::test_int(3),
|
"id" => Value::test_int(618),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
|
@ -15,7 +15,6 @@ mod find;
|
|||||||
mod first;
|
mod first;
|
||||||
mod flatten;
|
mod flatten;
|
||||||
mod get;
|
mod get;
|
||||||
mod group;
|
|
||||||
mod group_by;
|
mod group_by;
|
||||||
mod headers;
|
mod headers;
|
||||||
mod insert;
|
mod insert;
|
||||||
@ -73,7 +72,6 @@ pub use find::Find;
|
|||||||
pub use first::First;
|
pub use first::First;
|
||||||
pub use flatten::Flatten;
|
pub use flatten::Flatten;
|
||||||
pub use get::Get;
|
pub use get::Get;
|
||||||
pub use group::Group;
|
|
||||||
pub use group_by::GroupBy;
|
pub use group_by::GroupBy;
|
||||||
pub use headers::Headers;
|
pub use headers::Headers;
|
||||||
pub use insert::Insert;
|
pub use insert::Insert;
|
||||||
|
@ -25,7 +25,7 @@ impl Command for Range {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search_terms(&self) -> Vec<&str> {
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
vec!["filter", "head", "tail"]
|
vec!["filter", "head", "tail", "slice"]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
@ -64,12 +64,12 @@ produce a table, a list will produce a list, and a record will produce a record.
|
|||||||
Value::String { val, .. } => {
|
Value::String { val, .. } => {
|
||||||
let cv = CellPath {
|
let cv = CellPath {
|
||||||
members: vec![PathMember::String {
|
members: vec![PathMember::String {
|
||||||
val: val.clone(),
|
val,
|
||||||
span: *col_span,
|
span: *col_span,
|
||||||
optional: false,
|
optional: false,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
new_columns.push(cv.clone());
|
new_columns.push(cv);
|
||||||
}
|
}
|
||||||
Value::Int { val, internal_span } => {
|
Value::Int { val, internal_span } => {
|
||||||
if val < 0 {
|
if val < 0 {
|
||||||
@ -87,7 +87,7 @@ produce a table, a list will produce a list, and a record will produce a record.
|
|||||||
optional: false,
|
optional: false,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
new_columns.push(cv.clone());
|
new_columns.push(cv);
|
||||||
}
|
}
|
||||||
x => {
|
x => {
|
||||||
return Err(ShellError::CantConvert {
|
return Err(ShellError::CantConvert {
|
||||||
@ -240,7 +240,7 @@ fn select(
|
|||||||
//FIXME: improve implementation to not clone
|
//FIXME: improve implementation to not clone
|
||||||
match input_val.clone().follow_cell_path(&path.members, false) {
|
match input_val.clone().follow_cell_path(&path.members, false) {
|
||||||
Ok(fetcher) => {
|
Ok(fetcher) => {
|
||||||
record.push(path.to_string(), fetcher);
|
record.push(path.to_column_name(), fetcher);
|
||||||
if !columns_with_value.contains(&path) {
|
if !columns_with_value.contains(&path) {
|
||||||
columns_with_value.push(path);
|
columns_with_value.push(path);
|
||||||
}
|
}
|
||||||
@ -271,7 +271,7 @@ fn select(
|
|||||||
// FIXME: remove clone
|
// FIXME: remove clone
|
||||||
match v.clone().follow_cell_path(&cell_path.members, false) {
|
match v.clone().follow_cell_path(&cell_path.members, false) {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
record.push(cell_path.to_string(), result);
|
record.push(cell_path.to_column_name(), result);
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
@ -295,7 +295,7 @@ fn select(
|
|||||||
//FIXME: improve implementation to not clone
|
//FIXME: improve implementation to not clone
|
||||||
match x.clone().follow_cell_path(&path.members, false) {
|
match x.clone().follow_cell_path(&path.members, false) {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
record.push(path.to_string(), value);
|
record.push(path.to_column_name(), value);
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
use alphanumeric_sort::compare_str;
|
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::ast::PathMember;
|
||||||
|
|
||||||
use nu_utils::IgnoreCaseExt;
|
use crate::Comparator;
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Sort;
|
pub struct Sort;
|
||||||
@ -14,10 +13,13 @@ impl Command for Sort {
|
|||||||
|
|
||||||
fn signature(&self) -> nu_protocol::Signature {
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
Signature::build("sort")
|
Signature::build("sort")
|
||||||
.input_output_types(vec![(
|
.input_output_types(vec![
|
||||||
Type::List(Box::new(Type::Any)),
|
(
|
||||||
Type::List(Box::new(Type::Any)),
|
Type::List(Box::new(Type::Any)),
|
||||||
), (Type::record(), Type::record()),])
|
Type::List(Box::new(Type::Any))
|
||||||
|
),
|
||||||
|
(Type::record(), Type::record())
|
||||||
|
])
|
||||||
.switch("reverse", "Sort in reverse order", Some('r'))
|
.switch("reverse", "Sort in reverse order", Some('r'))
|
||||||
.switch(
|
.switch(
|
||||||
"ignore-case",
|
"ignore-case",
|
||||||
@ -45,67 +47,66 @@ impl Command for Sort {
|
|||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
example: "[2 0 1] | sort",
|
example: "[2 0 1] | sort",
|
||||||
description: "sort the list by increasing value",
|
description: "Sort the list by increasing value",
|
||||||
result: Some(Value::list(
|
result: Some(Value::test_list(vec![
|
||||||
vec![Value::test_int(0), Value::test_int(1), Value::test_int(2)],
|
Value::test_int(0),
|
||||||
Span::test_data(),
|
Value::test_int(1),
|
||||||
)),
|
Value::test_int(2),
|
||||||
|
])),
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
example: "[2 0 1] | sort --reverse",
|
example: "[2 0 1] | sort --reverse",
|
||||||
description: "sort the list by decreasing value",
|
description: "Sort the list by decreasing value",
|
||||||
result: Some(Value::list(
|
result: Some(Value::test_list(vec![
|
||||||
vec![Value::test_int(2), Value::test_int(1), Value::test_int(0)],
|
Value::test_int(2),
|
||||||
Span::test_data(),
|
Value::test_int(1),
|
||||||
)),
|
Value::test_int(0),
|
||||||
|
])),
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
example: "[betty amy sarah] | sort",
|
example: "[betty amy sarah] | sort",
|
||||||
description: "sort a list of strings",
|
description: "Sort a list of strings",
|
||||||
result: Some(Value::list(
|
result: Some(Value::test_list(vec![
|
||||||
vec![
|
Value::test_string("amy"),
|
||||||
Value::test_string("amy"),
|
Value::test_string("betty"),
|
||||||
Value::test_string("betty"),
|
Value::test_string("sarah"),
|
||||||
Value::test_string("sarah"),
|
])),
|
||||||
],
|
|
||||||
Span::test_data(),
|
|
||||||
)),
|
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
example: "[betty amy sarah] | sort --reverse",
|
example: "[betty amy sarah] | sort --reverse",
|
||||||
description: "sort a list of strings in reverse",
|
description: "Sort a list of strings in reverse",
|
||||||
result: Some(Value::list(
|
result: Some(Value::test_list(vec![
|
||||||
vec![
|
Value::test_string("sarah"),
|
||||||
Value::test_string("sarah"),
|
Value::test_string("betty"),
|
||||||
Value::test_string("betty"),
|
Value::test_string("amy"),
|
||||||
Value::test_string("amy"),
|
])),
|
||||||
],
|
|
||||||
Span::test_data(),
|
|
||||||
)),
|
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Sort strings (case-insensitive)",
|
description: "Sort strings (case-insensitive)",
|
||||||
example: "[airplane Truck Car] | sort -i",
|
example: "[airplane Truck Car] | sort -i",
|
||||||
result: Some(Value::list(
|
result: Some(Value::test_list(vec![
|
||||||
vec![
|
Value::test_string("airplane"),
|
||||||
Value::test_string("airplane"),
|
Value::test_string("Car"),
|
||||||
Value::test_string("Car"),
|
Value::test_string("Truck"),
|
||||||
Value::test_string("Truck"),
|
])),
|
||||||
],
|
|
||||||
Span::test_data(),
|
|
||||||
)),
|
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Sort strings (reversed case-insensitive)",
|
description: "Sort strings (reversed case-insensitive)",
|
||||||
example: "[airplane Truck Car] | sort -i -r",
|
example: "[airplane Truck Car] | sort -i -r",
|
||||||
result: Some(Value::list(
|
result: Some(Value::test_list(vec![
|
||||||
vec![
|
Value::test_string("Truck"),
|
||||||
Value::test_string("Truck"),
|
Value::test_string("Car"),
|
||||||
Value::test_string("Car"),
|
Value::test_string("airplane"),
|
||||||
Value::test_string("airplane"),
|
])),
|
||||||
],
|
},
|
||||||
Span::test_data(),
|
Example {
|
||||||
)),
|
description: "Sort alphanumeric strings in natural order",
|
||||||
|
example: "[foo1 foo10 foo9] | sort -n",
|
||||||
|
result: Some(Value::test_list(vec![
|
||||||
|
Value::test_string("foo1"),
|
||||||
|
Value::test_string("foo9"),
|
||||||
|
Value::test_string("foo10"),
|
||||||
|
])),
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Sort record by key (case-insensitive)",
|
description: "Sort record by key (case-insensitive)",
|
||||||
@ -134,233 +135,65 @@ impl Command for Sort {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
|
||||||
let reverse = call.has_flag(engine_state, stack, "reverse")?;
|
let reverse = call.has_flag(engine_state, stack, "reverse")?;
|
||||||
let insensitive = call.has_flag(engine_state, stack, "ignore-case")?;
|
let insensitive = call.has_flag(engine_state, stack, "ignore-case")?;
|
||||||
let natural = call.has_flag(engine_state, stack, "natural")?;
|
let natural = call.has_flag(engine_state, stack, "natural")?;
|
||||||
|
let sort_by_value = call.has_flag(engine_state, stack, "values")?;
|
||||||
let metadata = input.metadata();
|
let metadata = input.metadata();
|
||||||
|
|
||||||
let span = input.span().unwrap_or(call.head);
|
let span = input.span().unwrap_or(call.head);
|
||||||
match input {
|
let value = input.into_value(span)?;
|
||||||
// Records have two sorting methods, toggled by presence or absence of -v
|
let sorted: Value = match value {
|
||||||
PipelineData::Value(Value::Record { val, .. }, ..) => {
|
Value::Record { val, .. } => {
|
||||||
let sort_by_value = call.has_flag(engine_state, stack, "values")?;
|
// Records have two sorting methods, toggled by presence or absence of -v
|
||||||
let record = sort_record(
|
let record = crate::sort_record(
|
||||||
val.into_owned(),
|
val.into_owned(),
|
||||||
span,
|
|
||||||
sort_by_value,
|
sort_by_value,
|
||||||
reverse,
|
reverse,
|
||||||
insensitive,
|
insensitive,
|
||||||
natural,
|
natural,
|
||||||
);
|
)?;
|
||||||
Ok(record.into_pipeline_data())
|
Value::record(record, span)
|
||||||
}
|
}
|
||||||
// Other values are sorted here
|
value @ Value::List { .. } => {
|
||||||
PipelineData::Value(v, ..)
|
// If we have a table specifically, then we want to sort along each column.
|
||||||
if !matches!(v, Value::List { .. } | Value::Range { .. }) =>
|
// Record's PartialOrd impl dictates that columns are compared in alphabetical order,
|
||||||
{
|
// so we have to explicitly compare by each column.
|
||||||
Ok(v.into_pipeline_data())
|
let r#type = value.get_type();
|
||||||
}
|
let mut vec = value.into_list().expect("matched list above");
|
||||||
pipe_data => {
|
if let Type::Table(cols) = r#type {
|
||||||
let mut vec: Vec<_> = pipe_data.into_iter().collect();
|
let columns: Vec<Comparator> = cols
|
||||||
|
.iter()
|
||||||
sort(&mut vec, head, insensitive, natural)?;
|
.map(|col| vec![PathMember::string(col.0.clone(), false, Span::unknown())])
|
||||||
|
.map(|members| CellPath { members })
|
||||||
|
.map(Comparator::CellPath)
|
||||||
|
.collect();
|
||||||
|
crate::sort_by(&mut vec, columns, span, insensitive, natural)?;
|
||||||
|
} else {
|
||||||
|
crate::sort(&mut vec, insensitive, natural)?;
|
||||||
|
}
|
||||||
|
|
||||||
if reverse {
|
if reverse {
|
||||||
vec.reverse()
|
vec.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
let iter = vec.into_iter();
|
Value::list(vec, span)
|
||||||
Ok(iter.into_pipeline_data_with_metadata(
|
|
||||||
head,
|
|
||||||
engine_state.signals().clone(),
|
|
||||||
metadata,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
Value::Nothing { .. } => {
|
||||||
}
|
return Err(ShellError::PipelineEmpty {
|
||||||
}
|
dst_span: value.span(),
|
||||||
|
})
|
||||||
fn sort_record(
|
|
||||||
record: Record,
|
|
||||||
rec_span: Span,
|
|
||||||
sort_by_value: bool,
|
|
||||||
reverse: bool,
|
|
||||||
insensitive: bool,
|
|
||||||
natural: bool,
|
|
||||||
) -> Value {
|
|
||||||
let mut input_pairs: Vec<(String, Value)> = record.into_iter().collect();
|
|
||||||
input_pairs.sort_by(|a, b| {
|
|
||||||
// Extract the data (if sort_by_value) or the column names for comparison
|
|
||||||
let left_res = if sort_by_value {
|
|
||||||
match &a.1 {
|
|
||||||
Value::String { val, .. } => val.clone(),
|
|
||||||
val => {
|
|
||||||
if let Ok(val) = val.coerce_string() {
|
|
||||||
val
|
|
||||||
} else {
|
|
||||||
// Values that can't be turned to strings are disregarded by the sort
|
|
||||||
// (same as in sort_utils.rs)
|
|
||||||
return Ordering::Equal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
_ => {
|
||||||
a.0.clone()
|
return Err(ShellError::PipelineMismatch {
|
||||||
};
|
exp_input_type: "record or list".to_string(),
|
||||||
let right_res = if sort_by_value {
|
dst_span: call.head,
|
||||||
match &b.1 {
|
src_span: value.span(),
|
||||||
Value::String { val, .. } => val.clone(),
|
})
|
||||||
val => {
|
|
||||||
if let Ok(val) = val.coerce_string() {
|
|
||||||
val
|
|
||||||
} else {
|
|
||||||
// Values that can't be turned to strings are disregarded by the sort
|
|
||||||
// (same as in sort_utils.rs)
|
|
||||||
return Ordering::Equal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
b.0.clone()
|
|
||||||
};
|
};
|
||||||
|
Ok(sorted.into_pipeline_data_with_metadata(metadata))
|
||||||
// Fold case if case-insensitive
|
|
||||||
let left = if insensitive {
|
|
||||||
left_res.to_folded_case()
|
|
||||||
} else {
|
|
||||||
left_res
|
|
||||||
};
|
|
||||||
let right = if insensitive {
|
|
||||||
right_res.to_folded_case()
|
|
||||||
} else {
|
|
||||||
right_res
|
|
||||||
};
|
|
||||||
|
|
||||||
if natural {
|
|
||||||
compare_str(left, right)
|
|
||||||
} else {
|
|
||||||
left.cmp(&right)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if reverse {
|
|
||||||
input_pairs.reverse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Value::record(input_pairs.into_iter().collect(), rec_span)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sort(
|
|
||||||
vec: &mut [Value],
|
|
||||||
span: Span,
|
|
||||||
insensitive: bool,
|
|
||||||
natural: bool,
|
|
||||||
) -> Result<(), ShellError> {
|
|
||||||
match vec.first() {
|
|
||||||
Some(Value::Record { val, .. }) => {
|
|
||||||
let columns: Vec<String> = val.columns().cloned().collect();
|
|
||||||
vec.sort_by(|a, b| process(a, b, &columns, span, insensitive, natural));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
vec.sort_by(|a, b| {
|
|
||||||
let span_a = a.span();
|
|
||||||
let span_b = b.span();
|
|
||||||
if insensitive {
|
|
||||||
let folded_left = match a {
|
|
||||||
Value::String { val, .. } => Value::string(val.to_folded_case(), span_a),
|
|
||||||
_ => a.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let folded_right = match b {
|
|
||||||
Value::String { val, .. } => Value::string(val.to_folded_case(), span_b),
|
|
||||||
_ => b.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if natural {
|
|
||||||
match (
|
|
||||||
folded_left.coerce_into_string(),
|
|
||||||
folded_right.coerce_into_string(),
|
|
||||||
) {
|
|
||||||
(Ok(left), Ok(right)) => compare_str(left, right),
|
|
||||||
_ => Ordering::Equal,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
folded_left
|
|
||||||
.partial_cmp(&folded_right)
|
|
||||||
.unwrap_or(Ordering::Equal)
|
|
||||||
}
|
|
||||||
} else if natural {
|
|
||||||
match (a.coerce_str(), b.coerce_str()) {
|
|
||||||
(Ok(left), Ok(right)) => compare_str(left, right),
|
|
||||||
_ => Ordering::Equal,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
a.partial_cmp(b).unwrap_or(Ordering::Equal)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process(
|
|
||||||
left: &Value,
|
|
||||||
right: &Value,
|
|
||||||
columns: &[String],
|
|
||||||
span: Span,
|
|
||||||
insensitive: bool,
|
|
||||||
natural: bool,
|
|
||||||
) -> Ordering {
|
|
||||||
for column in columns {
|
|
||||||
let left_value = left.get_data_by_key(column);
|
|
||||||
|
|
||||||
let left_res = match left_value {
|
|
||||||
Some(left_res) => left_res,
|
|
||||||
None => Value::nothing(span),
|
|
||||||
};
|
|
||||||
|
|
||||||
let right_value = right.get_data_by_key(column);
|
|
||||||
|
|
||||||
let right_res = match right_value {
|
|
||||||
Some(right_res) => right_res,
|
|
||||||
None => Value::nothing(span),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = if insensitive {
|
|
||||||
let span_left = left_res.span();
|
|
||||||
let span_right = right_res.span();
|
|
||||||
let folded_left = match left_res {
|
|
||||||
Value::String { val, .. } => Value::string(val.to_folded_case(), span_left),
|
|
||||||
_ => left_res,
|
|
||||||
};
|
|
||||||
|
|
||||||
let folded_right = match right_res {
|
|
||||||
Value::String { val, .. } => Value::string(val.to_folded_case(), span_right),
|
|
||||||
_ => right_res,
|
|
||||||
};
|
|
||||||
if natural {
|
|
||||||
match (
|
|
||||||
folded_left.coerce_into_string(),
|
|
||||||
folded_right.coerce_into_string(),
|
|
||||||
) {
|
|
||||||
(Ok(left), Ok(right)) => compare_str(left, right),
|
|
||||||
_ => Ordering::Equal,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
folded_left
|
|
||||||
.partial_cmp(&folded_right)
|
|
||||||
.unwrap_or(Ordering::Equal)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
left_res.partial_cmp(&right_res).unwrap_or(Ordering::Equal)
|
|
||||||
};
|
|
||||||
if result != Ordering::Equal {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ordering::Equal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::{command_prelude::*, ClosureEval};
|
||||||
|
|
||||||
|
use crate::Comparator;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SortBy;
|
pub struct SortBy;
|
||||||
@ -18,24 +20,37 @@ impl Command for SortBy {
|
|||||||
(Type::record(), Type::table()),
|
(Type::record(), Type::table()),
|
||||||
(Type::table(), Type::table()),
|
(Type::table(), Type::table()),
|
||||||
])
|
])
|
||||||
.rest("columns", SyntaxShape::Any, "The column(s) to sort by.")
|
.rest(
|
||||||
|
"comparator",
|
||||||
|
SyntaxShape::OneOf(vec![
|
||||||
|
SyntaxShape::CellPath,
|
||||||
|
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), // key closure
|
||||||
|
SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Any])), // custom closure
|
||||||
|
]),
|
||||||
|
"The cell path(s) or closure(s) to compare elements by.",
|
||||||
|
)
|
||||||
.switch("reverse", "Sort in reverse order", Some('r'))
|
.switch("reverse", "Sort in reverse order", Some('r'))
|
||||||
.switch(
|
.switch(
|
||||||
"ignore-case",
|
"ignore-case",
|
||||||
"Sort string-based columns case-insensitively",
|
"Sort string-based data case-insensitively",
|
||||||
Some('i'),
|
Some('i'),
|
||||||
)
|
)
|
||||||
.switch(
|
.switch(
|
||||||
"natural",
|
"natural",
|
||||||
"Sort alphanumeric string-based columns naturally (1, 9, 10, 99, 100, ...)",
|
"Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...)",
|
||||||
Some('n'),
|
Some('n'),
|
||||||
)
|
)
|
||||||
|
.switch(
|
||||||
|
"custom",
|
||||||
|
"Use closures to specify a custom sort order, rather than to compute a comparison key",
|
||||||
|
Some('c'),
|
||||||
|
)
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.category(Category::Filters)
|
.category(Category::Filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"Sort by the given columns, in increasing order."
|
"Sort by the given cell path or closure."
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
@ -68,6 +83,41 @@ impl Command for SortBy {
|
|||||||
}),
|
}),
|
||||||
])),
|
])),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Sort by a nested value",
|
||||||
|
example: "[[name info]; [Cairo {founded: 969}] [Kyoto {founded: 794}]] | sort-by info.founded",
|
||||||
|
result: Some(Value::test_list(vec![
|
||||||
|
Value::test_record(record! {
|
||||||
|
"name" => Value::test_string("Kyoto"),
|
||||||
|
"info" => Value::test_record(
|
||||||
|
record! { "founded" => Value::test_int(794) },
|
||||||
|
)}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"name" => Value::test_string("Cairo"),
|
||||||
|
"info" => Value::test_record(
|
||||||
|
record! { "founded" => Value::test_int(969) },
|
||||||
|
)})
|
||||||
|
])),
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Sort by the last value in a list",
|
||||||
|
example: "[[2 50] [10 1]] | sort-by { last }",
|
||||||
|
result: Some(Value::test_list(vec![
|
||||||
|
Value::test_list(vec![Value::test_int(10), Value::test_int(1)]),
|
||||||
|
Value::test_list(vec![Value::test_int(2), Value::test_int(50)])
|
||||||
|
]))
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Sort in a custom order",
|
||||||
|
example: "[7 3 2 8 4] | sort-by -c {|a, b| $a < $b}",
|
||||||
|
result: Some(Value::test_list(vec![
|
||||||
|
Value::test_int(2),
|
||||||
|
Value::test_int(3),
|
||||||
|
Value::test_int(4),
|
||||||
|
Value::test_int(7),
|
||||||
|
Value::test_int(8),
|
||||||
|
]))
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,39 +129,60 @@ impl Command for SortBy {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
let columns: Vec<String> = call.rest(engine_state, stack, 0)?;
|
let comparator_vals: Vec<Value> = call.rest(engine_state, stack, 0)?;
|
||||||
let reverse = call.has_flag(engine_state, stack, "reverse")?;
|
let reverse = call.has_flag(engine_state, stack, "reverse")?;
|
||||||
let insensitive = call.has_flag(engine_state, stack, "ignore-case")?;
|
let insensitive = call.has_flag(engine_state, stack, "ignore-case")?;
|
||||||
let natural = call.has_flag(engine_state, stack, "natural")?;
|
let natural = call.has_flag(engine_state, stack, "natural")?;
|
||||||
|
let custom = call.has_flag(engine_state, stack, "custom")?;
|
||||||
let metadata = input.metadata();
|
let metadata = input.metadata();
|
||||||
let mut vec: Vec<_> = input.into_iter_strict(head)?.collect();
|
let mut vec: Vec<_> = input.into_iter_strict(head)?.collect();
|
||||||
|
|
||||||
if columns.is_empty() {
|
if comparator_vals.is_empty() {
|
||||||
return Err(ShellError::MissingParameter {
|
return Err(ShellError::MissingParameter {
|
||||||
param_name: "columns".into(),
|
param_name: "comparator".into(),
|
||||||
span: head,
|
span: head,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::sort(&mut vec, columns, head, insensitive, natural)?;
|
let comparators = comparator_vals
|
||||||
|
.into_iter()
|
||||||
|
.map(|val| match val {
|
||||||
|
Value::CellPath { val, .. } => Ok(Comparator::CellPath(val)),
|
||||||
|
Value::Closure { val, .. } => {
|
||||||
|
let closure_eval = ClosureEval::new(engine_state, stack, *val);
|
||||||
|
if custom {
|
||||||
|
Ok(Comparator::CustomClosure(closure_eval))
|
||||||
|
} else {
|
||||||
|
Ok(Comparator::KeyClosure(closure_eval))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(ShellError::TypeMismatch {
|
||||||
|
err_message: "Cannot sort using a value which is not a cell path or closure"
|
||||||
|
.into(),
|
||||||
|
span: val.span(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
crate::sort_by(&mut vec, comparators, head, insensitive, natural)?;
|
||||||
|
|
||||||
if reverse {
|
if reverse {
|
||||||
vec.reverse()
|
vec.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
let iter = vec.into_iter();
|
let val = Value::list(vec, head);
|
||||||
Ok(iter.into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata))
|
Ok(val.into_pipeline_data_with_metadata(metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use crate::{test_examples_with_commands, Last};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_examples() {
|
fn test_examples() {
|
||||||
use crate::test_examples;
|
test_examples_with_commands(SortBy {}, &[&Last]);
|
||||||
|
|
||||||
test_examples(SortBy {})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,16 +84,16 @@ pub fn split_by(
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let name = call.head;
|
let name = call.head;
|
||||||
|
let config = engine_state.get_config();
|
||||||
let splitter: Option<Value> = call.opt(engine_state, stack, 0)?;
|
let splitter: Option<Value> = call.opt(engine_state, stack, 0)?;
|
||||||
|
|
||||||
match splitter {
|
match splitter {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
let splitter = Some(Spanned {
|
let splitter = Some(Spanned {
|
||||||
item: v.coerce_into_string()?,
|
item: v.to_abbreviated_string(config),
|
||||||
span: name,
|
span: name,
|
||||||
});
|
});
|
||||||
Ok(split(splitter.as_ref(), input, name)?)
|
Ok(split(splitter.as_ref(), input, name, config)?)
|
||||||
}
|
}
|
||||||
// This uses the same format as the 'requires a column name' error in sort_utils.rs
|
// This uses the same format as the 'requires a column name' error in sort_utils.rs
|
||||||
None => Err(ShellError::GenericError {
|
None => Err(ShellError::GenericError {
|
||||||
@ -110,6 +110,7 @@ pub fn split(
|
|||||||
column_name: Option<&Spanned<String>>,
|
column_name: Option<&Spanned<String>>,
|
||||||
values: PipelineData,
|
values: PipelineData,
|
||||||
span: Span,
|
span: Span,
|
||||||
|
config: &nu_protocol::Config,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let grouper = if let Some(column_name) = column_name {
|
let grouper = if let Some(column_name) = column_name {
|
||||||
Grouper::ByColumn(Some(column_name.clone()))
|
Grouper::ByColumn(Some(column_name.clone()))
|
||||||
@ -127,7 +128,7 @@ pub fn split(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match group_key {
|
match group_key {
|
||||||
Some(group_key) => Ok(group_key.coerce_string()?),
|
Some(group_key) => Ok(group_key.to_abbreviated_string(config)),
|
||||||
None => Err(ShellError::CantFindColumn {
|
None => Err(ShellError::CantFindColumn {
|
||||||
col_name: column_name.item.to_string(),
|
col_name: column_name.item.to_string(),
|
||||||
span: Some(column_name.span),
|
span: Some(column_name.span),
|
||||||
@ -136,12 +137,12 @@ pub fn split(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
data_split(values, Some(&block), span)
|
data_split(values, Some(&block), span, config)
|
||||||
}
|
}
|
||||||
Grouper::ByColumn(None) => {
|
Grouper::ByColumn(None) => {
|
||||||
let block = move |_, row: &Value| row.coerce_string();
|
let block = move |_, row: &Value| Ok(row.to_abbreviated_string(config));
|
||||||
|
|
||||||
data_split(values, Some(&block), span)
|
data_split(values, Some(&block), span, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,6 +152,7 @@ fn data_group(
|
|||||||
values: &Value,
|
values: &Value,
|
||||||
grouper: Option<&dyn Fn(usize, &Value) -> Result<String, ShellError>>,
|
grouper: Option<&dyn Fn(usize, &Value) -> Result<String, ShellError>>,
|
||||||
span: Span,
|
span: Span,
|
||||||
|
config: &nu_protocol::Config,
|
||||||
) -> Result<Value, ShellError> {
|
) -> Result<Value, ShellError> {
|
||||||
let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
|
let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
|
||||||
|
|
||||||
@ -158,7 +160,7 @@ fn data_group(
|
|||||||
let group_key = if let Some(ref grouper) = grouper {
|
let group_key = if let Some(ref grouper) = grouper {
|
||||||
grouper(idx, &value)
|
grouper(idx, &value)
|
||||||
} else {
|
} else {
|
||||||
value.coerce_string()
|
Ok(value.to_abbreviated_string(config))
|
||||||
};
|
};
|
||||||
|
|
||||||
let group = groups.entry(group_key?).or_default();
|
let group = groups.entry(group_key?).or_default();
|
||||||
@ -179,6 +181,7 @@ pub fn data_split(
|
|||||||
value: PipelineData,
|
value: PipelineData,
|
||||||
splitter: Option<&dyn Fn(usize, &Value) -> Result<String, ShellError>>,
|
splitter: Option<&dyn Fn(usize, &Value) -> Result<String, ShellError>>,
|
||||||
dst_span: Span,
|
dst_span: Span,
|
||||||
|
config: &nu_protocol::Config,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let mut splits = indexmap::IndexMap::new();
|
let mut splits = indexmap::IndexMap::new();
|
||||||
|
|
||||||
@ -188,7 +191,7 @@ pub fn data_split(
|
|||||||
match v {
|
match v {
|
||||||
Value::Record { val: grouped, .. } => {
|
Value::Record { val: grouped, .. } => {
|
||||||
for (outer_key, list) in grouped.into_owned() {
|
for (outer_key, list) in grouped.into_owned() {
|
||||||
match data_group(&list, splitter, span) {
|
match data_group(&list, splitter, span, config) {
|
||||||
Ok(grouped_vals) => {
|
Ok(grouped_vals) => {
|
||||||
if let Value::Record { val: sub, .. } = grouped_vals {
|
if let Value::Record { val: sub, .. } = grouped_vals {
|
||||||
for (inner_key, subset) in sub.into_owned() {
|
for (inner_key, subset) in sub.into_owned() {
|
||||||
|
@ -158,12 +158,12 @@ use it in your pipeline."#
|
|||||||
let tee_thread = spawn_tee(info.clone(), eval_block)?;
|
let tee_thread = spawn_tee(info.clone(), eval_block)?;
|
||||||
let tee = IoTee::new(stderr, tee_thread);
|
let tee = IoTee::new(stderr, tee_thread);
|
||||||
match stack.stderr() {
|
match stack.stderr() {
|
||||||
OutDest::Pipe | OutDest::Capture => {
|
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value => {
|
||||||
child.stderr = Some(ChildPipe::Tee(Box::new(tee)));
|
child.stderr = Some(ChildPipe::Tee(Box::new(tee)));
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
OutDest::Null => copy_on_thread(tee, io::sink(), &info).map(Some),
|
OutDest::Null => copy_on_thread(tee, io::sink(), &info).map(Some),
|
||||||
OutDest::Inherit => {
|
OutDest::Print | OutDest::Inherit => {
|
||||||
copy_on_thread(tee, io::stderr(), &info).map(Some)
|
copy_on_thread(tee, io::stderr(), &info).map(Some)
|
||||||
}
|
}
|
||||||
OutDest::File(file) => {
|
OutDest::File(file) => {
|
||||||
@ -176,12 +176,14 @@ use it in your pipeline."#
|
|||||||
|
|
||||||
if let Some(stdout) = child.stdout.take() {
|
if let Some(stdout) = child.stdout.take() {
|
||||||
match stack.stdout() {
|
match stack.stdout() {
|
||||||
OutDest::Pipe | OutDest::Capture => {
|
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value => {
|
||||||
child.stdout = Some(stdout);
|
child.stdout = Some(stdout);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
OutDest::Null => copy_pipe(stdout, io::sink(), &info),
|
OutDest::Null => copy_pipe(stdout, io::sink(), &info),
|
||||||
OutDest::Inherit => copy_pipe(stdout, io::stdout(), &info),
|
OutDest::Print | OutDest::Inherit => {
|
||||||
|
copy_pipe(stdout, io::stdout(), &info)
|
||||||
|
}
|
||||||
OutDest::File(file) => copy_pipe(stdout, file.as_ref(), &info),
|
OutDest::File(file) => copy_pipe(stdout, file.as_ref(), &info),
|
||||||
}?;
|
}?;
|
||||||
}
|
}
|
||||||
@ -191,14 +193,14 @@ use it in your pipeline."#
|
|||||||
let stderr_thread = if let Some(stderr) = child.stderr.take() {
|
let stderr_thread = if let Some(stderr) = child.stderr.take() {
|
||||||
let info = info.clone();
|
let info = info.clone();
|
||||||
match stack.stderr() {
|
match stack.stderr() {
|
||||||
OutDest::Pipe | OutDest::Capture => {
|
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value => {
|
||||||
child.stderr = Some(stderr);
|
child.stderr = Some(stderr);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
OutDest::Null => {
|
OutDest::Null => {
|
||||||
copy_pipe_on_thread(stderr, io::sink(), &info).map(Some)
|
copy_pipe_on_thread(stderr, io::sink(), &info).map(Some)
|
||||||
}
|
}
|
||||||
OutDest::Inherit => {
|
OutDest::Print | OutDest::Inherit => {
|
||||||
copy_pipe_on_thread(stderr, io::stderr(), &info).map(Some)
|
copy_pipe_on_thread(stderr, io::stderr(), &info).map(Some)
|
||||||
}
|
}
|
||||||
OutDest::File(file) => {
|
OutDest::File(file) => {
|
||||||
@ -213,12 +215,12 @@ use it in your pipeline."#
|
|||||||
let tee_thread = spawn_tee(info.clone(), eval_block)?;
|
let tee_thread = spawn_tee(info.clone(), eval_block)?;
|
||||||
let tee = IoTee::new(stdout, tee_thread);
|
let tee = IoTee::new(stdout, tee_thread);
|
||||||
match stack.stdout() {
|
match stack.stdout() {
|
||||||
OutDest::Pipe | OutDest::Capture => {
|
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value => {
|
||||||
child.stdout = Some(ChildPipe::Tee(Box::new(tee)));
|
child.stdout = Some(ChildPipe::Tee(Box::new(tee)));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
OutDest::Null => copy(tee, io::sink(), &info),
|
OutDest::Null => copy(tee, io::sink(), &info),
|
||||||
OutDest::Inherit => copy(tee, io::stdout(), &info),
|
OutDest::Print | OutDest::Inherit => copy(tee, io::stdout(), &info),
|
||||||
OutDest::File(file) => copy(tee, file.as_ref(), &info),
|
OutDest::File(file) => copy(tee, file.as_ref(), &info),
|
||||||
}?;
|
}?;
|
||||||
}
|
}
|
||||||
@ -280,7 +282,7 @@ use it in your pipeline."#
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
|
fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
|
||||||
(Some(OutDest::Capture), Some(OutDest::Capture))
|
(Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ impl Command for Transpose {
|
|||||||
])
|
])
|
||||||
.switch(
|
.switch(
|
||||||
"header-row",
|
"header-row",
|
||||||
"treat the first row as column names",
|
"use the first input column as the table header-row (or keynames when combined with --as-record)",
|
||||||
Some('r'),
|
Some('r'),
|
||||||
)
|
)
|
||||||
.switch(
|
.switch(
|
||||||
@ -175,6 +175,12 @@ pub fn transpose(
|
|||||||
|
|
||||||
let metadata = input.metadata();
|
let metadata = input.metadata();
|
||||||
let input: Vec<_> = input.into_iter().collect();
|
let input: Vec<_> = input.into_iter().collect();
|
||||||
|
// Ensure error values are propagated
|
||||||
|
for i in input.iter() {
|
||||||
|
if let Value::Error { .. } = i {
|
||||||
|
return Ok(i.clone().into_pipeline_data_with_metadata(metadata));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let descs = get_columns(&input);
|
let descs = get_columns(&input);
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ impl Command for Uniq {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search_terms(&self) -> Vec<&str> {
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
vec!["distinct", "deduplicate"]
|
vec!["distinct", "deduplicate", "count"]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
@ -57,31 +57,34 @@ impl Command for Wrap {
|
|||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
description: "Wrap a list into a table with a given column name",
|
description: "Wrap a list into a table with a given column name",
|
||||||
example: "[1 2 3] | wrap num",
|
example: "[ Pachisi Mahjong Catan Carcassonne ] | wrap game",
|
||||||
result: Some(Value::test_list(vec![
|
result: Some(Value::test_list(vec![
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"num" => Value::test_int(1),
|
"game" => Value::test_string("Pachisi"),
|
||||||
}),
|
}),
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"num" => Value::test_int(2),
|
"game" => Value::test_string("Mahjong"),
|
||||||
}),
|
}),
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"num" => Value::test_int(3),
|
"game" => Value::test_string("Catan"),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"game" => Value::test_string("Carcassonne"),
|
||||||
}),
|
}),
|
||||||
])),
|
])),
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Wrap a range into a table with a given column name",
|
description: "Wrap a range into a table with a given column name",
|
||||||
example: "1..3 | wrap num",
|
example: "4..6 | wrap num",
|
||||||
result: Some(Value::test_list(vec![
|
result: Some(Value::test_list(vec![
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"num" => Value::test_int(1),
|
"num" => Value::test_int(4),
|
||||||
}),
|
}),
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"num" => Value::test_int(2),
|
"num" => Value::test_int(5),
|
||||||
}),
|
}),
|
||||||
Value::test_record(record! {
|
Value::test_record(record! {
|
||||||
"num" => Value::test_int(3),
|
"num" => Value::test_int(6),
|
||||||
}),
|
}),
|
||||||
])),
|
])),
|
||||||
},
|
},
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user