mirror of
https://github.com/nushell/nushell.git
synced 2025-07-01 07:00:37 +02:00
Compare commits
159 Commits
0.103.0
...
release/0.
Author | SHA1 | Date | |
---|---|---|---|
98521100ed | |||
985211a924 | |||
a3388b00ae | |||
eadb8da9f7 | |||
cda15d91dd | |||
651a8716fb | |||
a1b7574306 | |||
09f12b9c4a | |||
9ae74e3941 | |||
d8bec8668f | |||
12ccaf5e33 | |||
5fecf59f54 | |||
a3aae2d26c | |||
d1d6518ece | |||
2d868323b6 | |||
0389815137 | |||
11cdb94699 | |||
0ca5c2f135 | |||
715b0d90a9 | |||
05c36d1bc7 | |||
208ebeefab | |||
b33f4b7f55 | |||
f41b1460aa | |||
220858d641 | |||
db261e3ed9 | |||
82eb1c5584 | |||
6be291b00a | |||
7add38fe32 | |||
78903724f5 | |||
cb57f0a539 | |||
717081bd2f | |||
e1ffaf2548 | |||
1db4be12d1 | |||
6193679dfc | |||
a9657e17ad | |||
03d455a688 | |||
bae04352ca | |||
a1497716f1 | |||
b5b63d2bf9 | |||
5c59611083 | |||
1503ee09ba | |||
24dba9dc53 | |||
a2dc3e3b33 | |||
95998bdd53 | |||
bd5de023a1 | |||
38e761493d | |||
7fcebf37ec | |||
0e9927ea4d | |||
d273ce89df | |||
2dc5c19b71 | |||
669b44ad7d | |||
eff063822a | |||
9a5c4d36be | |||
cd4560e97a | |||
24cc2f9d87 | |||
8f81812ef9 | |||
2229370b13 | |||
a33650a69e | |||
56d7e4bb89 | |||
e5f589ccdd | |||
8c4d3eaa7e | |||
89322f59f2 | |||
4e307480e4 | |||
d601abaee0 | |||
ceaa0f9375 | |||
d31b7024d8 | |||
9dd30d7756 | |||
eff9305eb3 | |||
885b87a842 | |||
017daeed18 | |||
c8c018452f | |||
1a0778d77e | |||
d75aa7ed1b | |||
39edd7e080 | |||
61dbcf3de6 | |||
f8ed4b45fd | |||
7b57f132bb | |||
dfca117551 | |||
29eb109b1e | |||
70d8163181 | |||
e4cef8a154 | |||
15146e68ad | |||
b0f9cda9b5 | |||
173162df2e | |||
c0b944edb6 | |||
26699d96eb | |||
08940ba4f8 | |||
ecb9799b6a | |||
a886e30e04 | |||
147009a161 | |||
12a1eefe73 | |||
0f8f3bcf9a | |||
639f4bd499 | |||
e82df7c1c9 | |||
41f4d0dcbc | |||
eb2a91ea7c | |||
b81d46574c | |||
1c6c85d35d | |||
67ea25afca | |||
f25525be6c | |||
a72f94f452 | |||
210c6f1c43 | |||
0cd90e2388 | |||
7ca2a6f8ac | |||
237a685605 | |||
2bf0397d80 | |||
5ec823996a | |||
67b6188b19 | |||
df74a0c961 | |||
af6c4bdc9c | |||
d7f26b177a | |||
470d130289 | |||
a23e96c945 | |||
9ba16dbdaf | |||
43f9ec295f | |||
f39e5b3f37 | |||
6c0b65b570 | |||
1dcaffb792 | |||
ca4222277e | |||
5c2bcd068b | |||
9aba96604b | |||
7be90c2644 | |||
7e9e93cf82 | |||
6d1f7cb3e3 | |||
334cf1862a | |||
49d86855ce | |||
5fe97b8d59 | |||
2bad1371f0 | |||
3030608de0 | |||
5d32cd2c40 | |||
07be33c119 | |||
eaf522b41f | |||
e76586ede4 | |||
1979b61a92 | |||
02fcc485fb | |||
55e05be0d8 | |||
e10ac2ede6 | |||
bf1f2d5ebd | |||
6aed1b42ae | |||
f33a26123c | |||
7c160725ed | |||
5832823dff | |||
3fe355c4a6 | |||
dd56c813f9 | |||
7a6cfa24fc | |||
2ea2a904e8 | |||
dfba62da00 | |||
b241e9edd5 | |||
946cef77f1 | |||
c99c8119fe | |||
2b4914608e | |||
8b80ceac32 | |||
e89bb2ee96 | |||
862d53bb6e | |||
820d0c0959 | |||
968eb45fb2 | |||
2c1d261cca | |||
69d1c8e948 | |||
2c7ab6e898 |
40
.github/labeler.yml
vendored
Normal file
40
.github/labeler.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# A bot for automatically labelling pull requests
|
||||||
|
# See https://github.com/actions/labeler
|
||||||
|
|
||||||
|
dataframe:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- crates/nu_plugin_polars/**
|
||||||
|
|
||||||
|
std-library:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- crates/nu-std/**
|
||||||
|
|
||||||
|
ci:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- .github/workflows/**
|
||||||
|
|
||||||
|
|
||||||
|
LSP:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- crates/nu-lsp/**
|
||||||
|
|
||||||
|
parser:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- crates/nu-parser/**
|
||||||
|
|
||||||
|
pr:plugins:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
# plugins API
|
||||||
|
- crates/nu-plugin/**
|
||||||
|
- crates/nu-plugin-core/**
|
||||||
|
- crates/nu-plugin-engine/**
|
||||||
|
- crates/nu-plugin-protocol/**
|
||||||
|
- crates/nu-plugin-test-support/**
|
||||||
|
# specific plugins (like polars)
|
||||||
|
- crates/nu_plugin_*/**
|
19
.github/workflows/labels.yml
vendored
Normal file
19
.github/workflows/labels.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Automatically labels PRs based on the configuration file
|
||||||
|
# you are probably looking for 👉 `.github/labeler.yml`
|
||||||
|
name: Label PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request_target
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
triage:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository_owner == 'nushell'
|
||||||
|
steps:
|
||||||
|
- uses: actions/labeler@v5
|
||||||
|
with:
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
sync-labels: true
|
153
.github/workflows/nightly-build.yml
vendored
153
.github/workflows/nightly-build.yml
vendored
@ -4,6 +4,7 @@
|
|||||||
# 2. https://github.com/JasonEtco/create-an-issue
|
# 2. https://github.com/JasonEtco/create-an-issue
|
||||||
# 3. https://docs.github.com/en/actions/learn-github-actions/variables
|
# 3. https://docs.github.com/en/actions/learn-github-actions/variables
|
||||||
# 4. https://github.com/actions/github-script
|
# 4. https://github.com/actions/github-script
|
||||||
|
# 5. https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
|
||||||
#
|
#
|
||||||
name: Nightly Build
|
name: Nightly Build
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ on:
|
|||||||
# This schedule will run only from the default branch
|
# This schedule will run only from the default branch
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '15 0 * * *' # run at 00:15 AM UTC
|
- cron: '15 0 * * *' # run at 00:15 AM UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@ -25,6 +27,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# This job is required by the release job, so we should make it run both from Nushell repo and nightly repo
|
# This job is required by the release job, so we should make it run both from Nushell repo and nightly repo
|
||||||
# if: github.repository == 'nushell/nightly'
|
# if: github.repository == 'nushell/nightly'
|
||||||
|
# Map a step output to a job output
|
||||||
|
outputs:
|
||||||
|
skip: ${{ steps.vars.outputs.skip }}
|
||||||
|
build_date: ${{ steps.vars.outputs.build_date }}
|
||||||
|
nightly_tag: ${{ steps.vars.outputs.nightly_tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -39,7 +46,7 @@ jobs:
|
|||||||
uses: hustcer/setup-nu@v3
|
uses: hustcer/setup-nu@v3
|
||||||
if: github.repository == 'nushell/nightly'
|
if: github.repository == 'nushell/nightly'
|
||||||
with:
|
with:
|
||||||
version: 0.101.0
|
version: 0.103.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
|
||||||
@ -57,16 +64,53 @@ jobs:
|
|||||||
# All the changes will be overwritten by the upstream main branch
|
# All the changes will be overwritten by the upstream main branch
|
||||||
git reset --hard src/main
|
git reset --hard src/main
|
||||||
git push origin main -f
|
git push origin main -f
|
||||||
let sha_short = (git rev-parse --short origin/main | str trim | str substring 0..7)
|
|
||||||
let tag_name = $'nightly-($sha_short)'
|
- name: Create Tag and Output Tag Name
|
||||||
if (git ls-remote --tags origin $tag_name | is-empty) {
|
if: github.repository == 'nushell/nightly'
|
||||||
git tag -a $tag_name -m $'Nightly build from ($sha_short)'
|
id: vars
|
||||||
|
shell: nu {0}
|
||||||
|
run: |
|
||||||
|
let date = date now | format date %m%d
|
||||||
|
let version = open Cargo.toml | get package.version
|
||||||
|
let sha_short = (git rev-parse --short origin/main | str trim | str substring 0..6)
|
||||||
|
let latest_meta = http get https://api.github.com/repos/nushell/nightly/releases
|
||||||
|
| sort-by -r created_at
|
||||||
|
| where tag_name =~ nightly
|
||||||
|
| get tag_name?.0? | default ''
|
||||||
|
| parse '{version}-nightly.{build}+{hash}'
|
||||||
|
if ($latest_meta.0?.hash? | default '') == $sha_short {
|
||||||
|
print $'(ansi g)Latest nightly build is up-to-date, skip rebuilding.(ansi reset)'
|
||||||
|
$'skip=true(char nl)' o>> $env.GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
let prev_ver = $latest_meta.0?.version? | default '0.0.0'
|
||||||
|
let build = if ($latest_meta | is-empty) or ($version != $prev_ver) { 1 } else {
|
||||||
|
($latest_meta | get build?.0? | default 0 | into int) + 1
|
||||||
|
}
|
||||||
|
let nightly_tag = $'($version)-nightly.($build)+($sha_short)'
|
||||||
|
$'build_date=($date)(char nl)' o>> $env.GITHUB_OUTPUT
|
||||||
|
$'nightly_tag=($nightly_tag)(char nl)' o>> $env.GITHUB_OUTPUT
|
||||||
|
if (git ls-remote --tags origin $nightly_tag | is-empty) {
|
||||||
|
ls **/Cargo.toml | each {|file|
|
||||||
|
open --raw $file.name
|
||||||
|
| str replace --all $'version = "($version)"' $'version = "($version)-nightly.($build)"'
|
||||||
|
| save --force $file.name
|
||||||
|
}
|
||||||
|
# Disable the following two workflows for the automatic committed changes
|
||||||
|
rm .github/workflows/ci.yml
|
||||||
|
rm .github/workflows/audit.yml
|
||||||
|
|
||||||
|
git add .
|
||||||
|
git commit -m $'Update version to ($version)-nightly.($build)'
|
||||||
|
git tag -a $nightly_tag -m $'Nightly build from ($sha_short)'
|
||||||
git push origin --tags
|
git push origin --tags
|
||||||
|
git push origin main -f
|
||||||
}
|
}
|
||||||
|
|
||||||
standard:
|
release:
|
||||||
name: Nu
|
name: Nu
|
||||||
needs: prepare
|
needs: prepare
|
||||||
|
if: needs.prepare.outputs.skip != 'true'
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -83,24 +127,15 @@ jobs:
|
|||||||
- armv7-unknown-linux-musleabihf
|
- armv7-unknown-linux-musleabihf
|
||||||
- riscv64gc-unknown-linux-gnu
|
- riscv64gc-unknown-linux-gnu
|
||||||
- loongarch64-unknown-linux-gnu
|
- loongarch64-unknown-linux-gnu
|
||||||
extra: ['bin']
|
|
||||||
include:
|
include:
|
||||||
- target: aarch64-apple-darwin
|
- target: aarch64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
- target: x86_64-apple-darwin
|
- target: x86_64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
- target: x86_64-pc-windows-msvc
|
- target: x86_64-pc-windows-msvc
|
||||||
extra: 'bin'
|
|
||||||
os: windows-latest
|
|
||||||
- target: x86_64-pc-windows-msvc
|
|
||||||
extra: msi
|
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- target: aarch64-pc-windows-msvc
|
- target: aarch64-pc-windows-msvc
|
||||||
extra: 'bin'
|
os: windows-11-arm
|
||||||
os: windows-latest
|
|
||||||
- target: aarch64-pc-windows-msvc
|
|
||||||
extra: msi
|
|
||||||
os: windows-latest
|
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
- target: x86_64-unknown-linux-musl
|
- target: x86_64-unknown-linux-musl
|
||||||
@ -119,40 +154,64 @@ jobs:
|
|||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
|
|
||||||
runs-on: ${{matrix.os}}
|
runs-on: ${{matrix.os}}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Wix Toolset 6 for Windows
|
||||||
|
shell: pwsh
|
||||||
|
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
|
run: |
|
||||||
|
dotnet tool install --global wix --version 6.0.0
|
||||||
|
dotnet workload install wix
|
||||||
|
$wixPath = "$env:USERPROFILE\.dotnet\tools"
|
||||||
|
echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
$env:PATH = "$wixPath;$env:PATH"
|
||||||
|
wix --version
|
||||||
|
|
||||||
- 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 and cache
|
- name: Setup Rust toolchain and cache
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
# 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
|
uses: hustcer/setup-nu@v3
|
||||||
|
if: ${{ matrix.os != 'windows-11-arm' }}
|
||||||
with:
|
with:
|
||||||
version: 0.101.0
|
version: 0.103.0
|
||||||
|
|
||||||
- name: Release Nu Binary
|
- name: Release Nu Binary
|
||||||
id: nu
|
id: nu
|
||||||
|
if: ${{ matrix.os != 'windows-11-arm' }}
|
||||||
run: nu .github/workflows/release-pkg.nu
|
run: nu .github/workflows/release-pkg.nu
|
||||||
env:
|
env:
|
||||||
OS: ${{ matrix.os }}
|
OS: ${{ matrix.os }}
|
||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
_EXTRA_: ${{ matrix.extra }}
|
|
||||||
|
- name: Build Nu for Windows ARM64
|
||||||
|
id: nu0
|
||||||
|
shell: pwsh
|
||||||
|
if: ${{ matrix.os == 'windows-11-arm' }}
|
||||||
|
run: |
|
||||||
|
$env:OS = 'windows'
|
||||||
|
$env:REF = '${{ github.ref }}'
|
||||||
|
$env:TARGET = '${{ matrix.target }}'
|
||||||
|
cargo build --release --all --target aarch64-pc-windows-msvc
|
||||||
|
cp ./target/${{ matrix.target }}/release/nu.exe .
|
||||||
|
./nu.exe -c 'version'
|
||||||
|
./nu.exe ${{github.workspace}}/.github/workflows/release-pkg.nu
|
||||||
|
|
||||||
- name: Create an Issue for Release Failure
|
- name: Create an Issue for Release Failure
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
uses: JasonEtco/create-an-issue@v2.9.2
|
uses: JasonEtco/create-an-issue@v2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@ -160,13 +219,6 @@ jobs:
|
|||||||
search_existing: open
|
search_existing: open
|
||||||
filename: .github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md
|
filename: .github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md
|
||||||
|
|
||||||
- name: Set Outputs of Short SHA
|
|
||||||
id: vars
|
|
||||||
run: |
|
|
||||||
echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
|
||||||
sha_short=$(git rev-parse --short HEAD)
|
|
||||||
echo "sha_short=${sha_short:0:7}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# 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
|
||||||
@ -174,9 +226,39 @@ jobs:
|
|||||||
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
|
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: ${{ steps.nu.outputs.archive }}
|
files: |
|
||||||
tag_name: nightly-${{ steps.vars.outputs.sha_short }}
|
${{ steps.nu.outputs.msi }}
|
||||||
name: Nu-nightly-${{ steps.vars.outputs.date }}-${{ steps.vars.outputs.sha_short }}
|
${{ steps.nu0.outputs.msi }}
|
||||||
|
${{ steps.nu.outputs.archive }}
|
||||||
|
${{ steps.nu0.outputs.archive }}
|
||||||
|
tag_name: ${{ needs.prepare.outputs.nightly_tag }}
|
||||||
|
name: ${{ needs.prepare.outputs.build_date }}-${{ needs.prepare.outputs.nightly_tag }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
sha256sum:
|
||||||
|
needs: [prepare, release]
|
||||||
|
name: Create Sha256sum
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'nushell/nightly'
|
||||||
|
steps:
|
||||||
|
- name: Download Release Archives
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: >-
|
||||||
|
gh release download ${{ needs.prepare.outputs.nightly_tag }}
|
||||||
|
--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.9
|
||||||
|
with:
|
||||||
|
draft: false
|
||||||
|
prerelease: true
|
||||||
|
files: SHA256SUMS
|
||||||
|
tag_name: ${{ needs.prepare.outputs.nightly_tag }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@ -184,12 +266,9 @@ jobs:
|
|||||||
name: Cleanup
|
name: Cleanup
|
||||||
# Should only run in nushell/nightly repo
|
# Should only run in nushell/nightly repo
|
||||||
if: github.repository == 'nushell/nightly'
|
if: github.repository == 'nushell/nightly'
|
||||||
|
needs: [release, sha256sum]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Sleep for 30 minutes, waiting for the release to be published
|
|
||||||
- name: Waiting for Release
|
|
||||||
run: sleep 1800
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
@ -197,14 +276,14 @@ jobs:
|
|||||||
- name: Setup Nushell
|
- name: Setup Nushell
|
||||||
uses: hustcer/setup-nu@v3
|
uses: hustcer/setup-nu@v3
|
||||||
with:
|
with:
|
||||||
version: 0.101.0
|
version: 0.103.0
|
||||||
|
|
||||||
# Keep the last a few releases
|
# Keep the last a few releases
|
||||||
- name: Delete Older Releases
|
- name: Delete Older Releases
|
||||||
shell: nu {0}
|
shell: nu {0}
|
||||||
run: |
|
run: |
|
||||||
let KEEP_COUNT = 10
|
let KEEP_COUNT = 10
|
||||||
let deprecated = (http get https://api.github.com/repos/nushell/nightly/releases | sort-by -r created_at | select tag_name id | range $KEEP_COUNT..)
|
let deprecated = (http get https://api.github.com/repos/nushell/nightly/releases | sort-by -r created_at | select tag_name id | slice $KEEP_COUNT..)
|
||||||
for release in $deprecated {
|
for release in $deprecated {
|
||||||
print $'Deleting tag ($release.tag_name)'
|
print $'Deleting tag ($release.tag_name)'
|
||||||
git push origin --delete $release.tag_name
|
git push origin --delete $release.tag_name
|
||||||
|
89
.github/workflows/release-pkg.nu
vendored
89
.github/workflows/release-pkg.nu
vendored
@ -117,14 +117,14 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
|
|||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Build for Windows without static-link-openssl feature
|
# Build for Windows without static-link-openssl feature
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
if $os in ['windows-latest'] {
|
if $os =~ 'windows' {
|
||||||
cargo-build-nu
|
cargo-build-nu
|
||||||
}
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Prepare for the release archive
|
# Prepare for the release archive
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
let suffix = if $os == 'windows-latest' { '.exe' }
|
let suffix = if $os =~ 'windows' { '.exe' }
|
||||||
# nu, nu_plugin_* were all included
|
# nu, nu_plugin_* were all included
|
||||||
let executable = $'target/($target)/release/($bin)*($suffix)'
|
let executable = $'target/($target)/release/($bin)*($suffix)'
|
||||||
print $'Current executable file: ($executable)'
|
print $'Current executable file: ($executable)'
|
||||||
@ -148,10 +148,10 @@ For more information, refer to https://www.nushell.sh/book/plugins.html
|
|||||||
[LICENSE ...(glob $executable)] | each {|it| cp -rv $it $dist } | flatten
|
[LICENSE ...(glob $executable)] | each {|it| cp -rv $it $dist } | flatten
|
||||||
|
|
||||||
print $'(char nl)Check binary release version detail:'; hr-line
|
print $'(char nl)Check binary release version detail:'; hr-line
|
||||||
let ver = if $os == 'windows-latest' {
|
let ver = if $os =~ 'windows' {
|
||||||
(do -i { .\output\nu.exe -c 'version' }) | str join
|
(do -i { .\output\nu.exe -c 'version' }) | default '' | str join
|
||||||
} else {
|
} else {
|
||||||
(do -i { ./output/nu -c 'version' }) | str join
|
(do -i { ./output/nu -c 'version' }) | default '' | str join
|
||||||
}
|
}
|
||||||
if ($ver | str trim | is-empty) {
|
if ($ver | str trim | is-empty) {
|
||||||
print $'(ansi r)Incompatible Nu binary: The binary cross compiled is not runnable on current arch...(ansi reset)'
|
print $'(ansi r)Incompatible Nu binary: The binary cross compiled is not runnable on current arch...(ansi reset)'
|
||||||
@ -175,53 +175,60 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
|
|||||||
tar -czf $archive $dest
|
tar -czf $archive $dest
|
||||||
print $'archive: ---> ($archive)'; ls $archive
|
print $'archive: ---> ($archive)'; ls $archive
|
||||||
# REF: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
|
# REF: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
|
||||||
echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
|
echo $"archive=($archive)(char nl)" o>> $env.GITHUB_OUTPUT
|
||||||
|
|
||||||
} else if $os == 'windows-latest' {
|
} else if $os =~ 'windows' {
|
||||||
|
|
||||||
let releaseStem = $'($bin)-($version)-($target)'
|
let releaseStem = $'($bin)-($version)-($target)'
|
||||||
|
let arch = if $nu.os-info.arch =~ 'x86_64' { 'x64' } else { 'arm64' }
|
||||||
|
fetch-less $arch
|
||||||
|
|
||||||
print $'(char nl)Download less related stuffs...'; hr-line
|
print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls | print
|
||||||
# todo: less-v661 is out but is released as a zip file. maybe we should switch to that and extract it?
|
let archive = $'($dist)/($releaseStem).zip'
|
||||||
aria2c https://github.com/jftuga/less-Windows/releases/download/less-v608/less.exe -o less.exe
|
7z a $archive ...(glob *)
|
||||||
# the below was renamed because it was failing to download for darren. it should work but it wasn't
|
let pkg = (ls -f $archive | get name)
|
||||||
# todo: maybe we should get rid of this aria2c dependency and just use http get?
|
if not ($pkg | is-empty) {
|
||||||
#aria2c https://raw.githubusercontent.com/jftuga/less-Windows/master/LICENSE -o LICENSE-for-less.txt
|
|
||||||
aria2c https://github.com/jftuga/less-Windows/blob/master/LICENSE -o LICENSE-for-less.txt
|
|
||||||
|
|
||||||
# Create Windows msi release package
|
|
||||||
if (get-env _EXTRA_) == 'msi' {
|
|
||||||
|
|
||||||
let wixRelease = $'($src)/target/wix/($releaseStem).msi'
|
|
||||||
print $'(char nl)Start creating Windows msi package with the following contents...'
|
|
||||||
cd $src; hr-line
|
|
||||||
# Wix need the binaries be stored in target/release/
|
|
||||||
cp -r ($'($dist)/*' | into glob) target/release/
|
|
||||||
ls target/release/* | print
|
|
||||||
cargo install cargo-wix --version 0.3.8
|
|
||||||
cargo wix --no-build --nocapture --package nu --output $wixRelease
|
|
||||||
# Workaround for https://github.com/softprops/action-gh-release/issues/280
|
# Workaround for https://github.com/softprops/action-gh-release/issues/280
|
||||||
let archive = ($wixRelease | str replace --all '\' '/')
|
let archive = ($pkg | get 0 | str replace --all '\' '/')
|
||||||
print $'archive: ---> ($archive)';
|
print $'archive: ---> ($archive)'
|
||||||
echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
|
echo $"archive=($archive)(char nl)" o>> $env.GITHUB_OUTPUT
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
# Create extra Windows msi release package if dotnet and wix are available
|
||||||
|
let installed = [dotnet wix] | all { (which $in | length) > 0 }
|
||||||
|
if $installed and (wix --version | split row . | first | into int) >= 6 {
|
||||||
|
|
||||||
print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls | print
|
print $'(char nl)Start creating Windows msi package with the following contents...'
|
||||||
let archive = $'($dist)/($releaseStem).zip'
|
cd $src; cd wix; hr-line; mkdir nu
|
||||||
7z a $archive ...(glob *)
|
# Wix need the binaries be stored in nu folder
|
||||||
let pkg = (ls -f $archive | get name)
|
cp -r ($'($dist)/*' | into glob) nu/
|
||||||
if not ($pkg | is-empty) {
|
cp $'($dist)/README.txt' .
|
||||||
# Workaround for https://github.com/softprops/action-gh-release/issues/280
|
ls -f nu/* | print
|
||||||
let archive = ($pkg | get 0 | str replace --all '\' '/')
|
./nu/nu.exe -c $'NU_RELEASE_VERSION=($version) dotnet build -c Release -p:Platform=($arch)'
|
||||||
print $'archive: ---> ($archive)'
|
glob **/*.msi | print
|
||||||
echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
|
# Workaround for https://github.com/softprops/action-gh-release/issues/280
|
||||||
}
|
let wixRelease = (glob **/*.msi | where $it =~ bin | get 0 | str replace --all '\' '/')
|
||||||
|
let msi = $'($wixRelease | path dirname)/nu-($version)-($target).msi'
|
||||||
|
mv $wixRelease $msi
|
||||||
|
print $'MSI archive: ---> ($msi)';
|
||||||
|
echo $"msi=($msi)(char nl)" o>> $env.GITHUB_OUTPUT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def fetch-less [
|
||||||
|
arch: string = 'x64' # The architecture to fetch
|
||||||
|
] {
|
||||||
|
let less_zip = $'less-($arch).zip'
|
||||||
|
print $'Fetching less archive: (ansi g)($less_zip)(ansi reset)'
|
||||||
|
let url = $'https://github.com/jftuga/less-Windows/releases/download/less-v668/($less_zip)'
|
||||||
|
http get https://github.com/jftuga/less-Windows/blob/master/LICENSE | save -rf LICENSE-for-less.txt
|
||||||
|
http get $url | save -rf $less_zip
|
||||||
|
unzip $less_zip
|
||||||
|
rm $less_zip lesskey.exe
|
||||||
|
}
|
||||||
|
|
||||||
def 'cargo-build-nu' [] {
|
def 'cargo-build-nu' [] {
|
||||||
if $os == 'windows-latest' {
|
if $os =~ 'windows' {
|
||||||
cargo build --release --all --target $target
|
cargo build --release --all --target $target
|
||||||
} else {
|
} else {
|
||||||
cargo build --release --all --target $target --features=static-link-openssl
|
cargo build --release --all --target $target --features=static-link-openssl
|
||||||
|
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
@ -35,24 +35,15 @@ jobs:
|
|||||||
- armv7-unknown-linux-musleabihf
|
- armv7-unknown-linux-musleabihf
|
||||||
- riscv64gc-unknown-linux-gnu
|
- riscv64gc-unknown-linux-gnu
|
||||||
- loongarch64-unknown-linux-gnu
|
- loongarch64-unknown-linux-gnu
|
||||||
extra: ['bin']
|
|
||||||
include:
|
include:
|
||||||
- target: aarch64-apple-darwin
|
- target: aarch64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
- target: x86_64-apple-darwin
|
- target: x86_64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
- target: x86_64-pc-windows-msvc
|
- target: x86_64-pc-windows-msvc
|
||||||
extra: 'bin'
|
|
||||||
os: windows-latest
|
|
||||||
- target: x86_64-pc-windows-msvc
|
|
||||||
extra: msi
|
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- target: aarch64-pc-windows-msvc
|
- target: aarch64-pc-windows-msvc
|
||||||
extra: 'bin'
|
os: windows-11-arm
|
||||||
os: windows-latest
|
|
||||||
- target: aarch64-pc-windows-msvc
|
|
||||||
extra: msi
|
|
||||||
os: windows-latest
|
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
- target: x86_64-unknown-linux-musl
|
- target: x86_64-unknown-linux-musl
|
||||||
@ -75,12 +66,23 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Wix Toolset 6 for Windows
|
||||||
|
shell: pwsh
|
||||||
|
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
|
run: |
|
||||||
|
dotnet tool install --global wix --version 6.0.0
|
||||||
|
dotnet workload install wix
|
||||||
|
$wixPath = "$env:USERPROFILE\.dotnet\tools"
|
||||||
|
echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
$env:PATH = "$wixPath;$env:PATH"
|
||||||
|
wix --version
|
||||||
|
|
||||||
- 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.11.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
||||||
# 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
|
||||||
@ -88,17 +90,31 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Nushell
|
- name: Setup Nushell
|
||||||
uses: hustcer/setup-nu@v3
|
uses: hustcer/setup-nu@v3
|
||||||
|
if: ${{ matrix.os != 'windows-11-arm' }}
|
||||||
with:
|
with:
|
||||||
version: 0.101.0
|
version: 0.103.0
|
||||||
|
|
||||||
- name: Release Nu Binary
|
- name: Release Nu Binary
|
||||||
id: nu
|
id: nu
|
||||||
|
if: ${{ matrix.os != 'windows-11-arm' }}
|
||||||
run: nu .github/workflows/release-pkg.nu
|
run: nu .github/workflows/release-pkg.nu
|
||||||
env:
|
env:
|
||||||
OS: ${{ matrix.os }}
|
OS: ${{ matrix.os }}
|
||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
_EXTRA_: ${{ matrix.extra }}
|
|
||||||
|
- name: Build Nu for Windows ARM64
|
||||||
|
id: nu0
|
||||||
|
shell: pwsh
|
||||||
|
if: ${{ matrix.os == 'windows-11-arm' }}
|
||||||
|
run: |
|
||||||
|
$env:OS = 'windows'
|
||||||
|
$env:REF = '${{ github.ref }}'
|
||||||
|
$env:TARGET = '${{ matrix.target }}'
|
||||||
|
cargo build --release --all --target aarch64-pc-windows-msvc
|
||||||
|
cp ./target/${{ matrix.target }}/release/nu.exe .
|
||||||
|
./nu.exe -c 'version'
|
||||||
|
./nu.exe ${{github.workspace}}/.github/workflows/release-pkg.nu
|
||||||
|
|
||||||
# WARN: Don't upgrade this action due to the release per asset issue.
|
# WARN: Don't upgrade this action due to the release per asset issue.
|
||||||
# See: https://github.com/softprops/action-gh-release/issues/445
|
# See: https://github.com/softprops/action-gh-release/issues/445
|
||||||
@ -107,7 +123,11 @@ jobs:
|
|||||||
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.msi }}
|
||||||
|
${{ steps.nu0.outputs.msi }}
|
||||||
|
${{ steps.nu.outputs.archive }}
|
||||||
|
${{ steps.nu0.outputs.archive }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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.29.10
|
uses: crate-ci/typos@v1.31.1
|
||||||
|
2
.github/workflows/winget-submission.yml
vendored
2
.github/workflows/winget-submission.yml
vendored
@ -26,4 +26,4 @@ jobs:
|
|||||||
version: ${{ inputs.tag_name || github.event.release.tag_name }}
|
version: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||||
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
|
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||||
token: ${{ secrets.NUSHELL_PAT }}
|
token: ${{ secrets.NUSHELL_PAT }}
|
||||||
fork-user: fdncred
|
fork-user: nushell
|
||||||
|
503
Cargo.lock
generated
503
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
84
Cargo.toml
84
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.83.0"
|
rust-version = "1.84.1"
|
||||||
version = "0.103.0"
|
version = "0.104.1"
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
@ -70,8 +70,8 @@ bracoxide = "0.1.5"
|
|||||||
brotli = "7.0"
|
brotli = "7.0"
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytesize = "1.3.1"
|
bytesize = "1.3.3"
|
||||||
calamine = "0.26.1"
|
calamine = "0.27"
|
||||||
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"
|
||||||
@ -91,8 +91,8 @@ fancy-regex = "0.14"
|
|||||||
filesize = "0.2"
|
filesize = "0.2"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
human-date-parser = "0.2.0"
|
human-date-parser = "0.3.0"
|
||||||
indexmap = "2.7"
|
indexmap = "2.9"
|
||||||
indicatif = "0.17"
|
indicatif = "0.17"
|
||||||
interprocess = "2.2.0"
|
interprocess = "2.2.0"
|
||||||
is_executable = "1.0"
|
is_executable = "1.0"
|
||||||
@ -110,7 +110,7 @@ md5 = { version = "0.10", package = "md-5" }
|
|||||||
miette = "7.5"
|
miette = "7.5"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
mockito = { version = "1.6", default-features = false }
|
mockito = { version = "1.7", default-features = false }
|
||||||
multipart-rs = "0.1.13"
|
multipart-rs = "0.1.13"
|
||||||
native-tls = "0.2"
|
native-tls = "0.2"
|
||||||
nix = { version = "0.29", default-features = false }
|
nix = { version = "0.29", default-features = false }
|
||||||
@ -135,22 +135,22 @@ quick-xml = "0.37.0"
|
|||||||
quickcheck = "1.0"
|
quickcheck = "1.0"
|
||||||
quickcheck_macros = "1.0"
|
quickcheck_macros = "1.0"
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
rand = "0.8"
|
rand = "0.9"
|
||||||
getrandom = "0.2" # pick same version that rand requires
|
getrandom = "0.2" # pick same version that rand requires
|
||||||
rand_chacha = "0.3.1"
|
rand_chacha = "0.9"
|
||||||
ratatui = "0.29"
|
ratatui = "0.29"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
reedline = "0.39.0"
|
reedline = "0.40.0"
|
||||||
rmp = "0.8"
|
rmp = "0.8"
|
||||||
rmp-serde = "1.3"
|
rmp-serde = "1.3"
|
||||||
roxmltree = "0.20"
|
roxmltree = "0.20"
|
||||||
rstest = { version = "0.23", default-features = false }
|
rstest = { version = "0.23", default-features = false }
|
||||||
rstest_reuse = "0.7"
|
rstest_reuse = "0.7"
|
||||||
rusqlite = "0.31"
|
rusqlite = "0.31"
|
||||||
rust-embed = "8.6.0"
|
rust-embed = "8.7.0"
|
||||||
scopeguard = { version = "1.2.0" }
|
scopeguard = { version = "1.2.0" }
|
||||||
serde = { version = "1.0" }
|
serde = { version = "1.0" }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0.97"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
serde_yaml = "0.9.33"
|
serde_yaml = "0.9.33"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
@ -161,24 +161,24 @@ syn = "2.0"
|
|||||||
sysinfo = "0.33"
|
sysinfo = "0.33"
|
||||||
tabled = { version = "0.17.0", default-features = false }
|
tabled = { version = "0.17.0", default-features = false }
|
||||||
tempfile = "3.15"
|
tempfile = "3.15"
|
||||||
titlecase = "3.4"
|
titlecase = "3.5"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
trash = "5.2"
|
trash = "5.2"
|
||||||
update-informer = { version = "1.2.0", default-features = false, features = ["github", "native-tls", "ureq"] }
|
update-informer = { version = "1.2.0", default-features = false, features = ["github", "native-tls", "ureq"] }
|
||||||
umask = "2.1"
|
umask = "2.1"
|
||||||
unicode-segmentation = "1.12"
|
unicode-segmentation = "1.12"
|
||||||
unicode-width = "0.2"
|
unicode-width = "0.2"
|
||||||
ureq = { version = "2.12", default-features = false }
|
ureq = { version = "2.12", default-features = false, features = ["socks-proxy"] }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
uu_cp = "0.0.29"
|
uu_cp = "0.0.30"
|
||||||
uu_mkdir = "0.0.29"
|
uu_mkdir = "0.0.30"
|
||||||
uu_mktemp = "0.0.29"
|
uu_mktemp = "0.0.30"
|
||||||
uu_mv = "0.0.29"
|
uu_mv = "0.0.30"
|
||||||
uu_touch = "0.0.29"
|
uu_touch = "0.0.30"
|
||||||
uu_whoami = "0.0.29"
|
uu_whoami = "0.0.30"
|
||||||
uu_uname = "0.0.29"
|
uu_uname = "0.0.30"
|
||||||
uucore = "0.0.29"
|
uucore = "0.0.30"
|
||||||
uuid = "1.12.0"
|
uuid = "1.16.0"
|
||||||
v_htmlescape = "0.15.0"
|
v_htmlescape = "0.15.0"
|
||||||
wax = "0.6"
|
wax = "0.6"
|
||||||
web-time = "1.1.0"
|
web-time = "1.1.0"
|
||||||
@ -197,22 +197,22 @@ unchecked_duration_subtraction = "warn"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-cli = { path = "./crates/nu-cli", version = "0.103.0" }
|
nu-cli = { path = "./crates/nu-cli", version = "0.104.1" }
|
||||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.103.0" }
|
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.104.1" }
|
||||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.103.0" }
|
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.104.1" }
|
||||||
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.103.0", optional = true }
|
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.104.1", optional = true }
|
||||||
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.103.0" }
|
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.104.1" }
|
||||||
nu-command = { path = "./crates/nu-command", version = "0.103.0" }
|
nu-command = { path = "./crates/nu-command", version = "0.104.1" }
|
||||||
nu-engine = { path = "./crates/nu-engine", version = "0.103.0" }
|
nu-engine = { path = "./crates/nu-engine", version = "0.104.1" }
|
||||||
nu-explore = { path = "./crates/nu-explore", version = "0.103.0" }
|
nu-explore = { path = "./crates/nu-explore", version = "0.104.1" }
|
||||||
nu-lsp = { path = "./crates/nu-lsp/", version = "0.103.0" }
|
nu-lsp = { path = "./crates/nu-lsp/", version = "0.104.1" }
|
||||||
nu-parser = { path = "./crates/nu-parser", version = "0.103.0" }
|
nu-parser = { path = "./crates/nu-parser", version = "0.104.1" }
|
||||||
nu-path = { path = "./crates/nu-path", version = "0.103.0" }
|
nu-path = { path = "./crates/nu-path", version = "0.104.1" }
|
||||||
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.103.0" }
|
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.104.1" }
|
||||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.103.0" }
|
nu-protocol = { path = "./crates/nu-protocol", version = "0.104.1" }
|
||||||
nu-std = { path = "./crates/nu-std", version = "0.103.0" }
|
nu-std = { path = "./crates/nu-std", version = "0.104.1" }
|
||||||
nu-system = { path = "./crates/nu-system", version = "0.103.0" }
|
nu-system = { path = "./crates/nu-system", version = "0.104.1" }
|
||||||
nu-utils = { path = "./crates/nu-utils", version = "0.103.0" }
|
nu-utils = { path = "./crates/nu-utils", version = "0.104.1" }
|
||||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||||
|
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
@ -241,9 +241,9 @@ nix = { workspace = true, default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-test-support = { path = "./crates/nu-test-support", version = "0.103.0" }
|
nu-test-support = { path = "./crates/nu-test-support", version = "0.104.1" }
|
||||||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.103.0" }
|
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.104.1" }
|
||||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.103.0" }
|
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.104.1" }
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
tango-bench = "0.6"
|
tango-bench = "0.6"
|
||||||
|
@ -5,28 +5,29 @@ 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.103.0"
|
version = "0.104.1"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" }
|
||||||
nu-command = { path = "../nu-command", version = "0.103.0" }
|
nu-command = { path = "../nu-command", version = "0.104.1" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
|
nu-std = { path = "../nu-std", version = "0.104.1" }
|
||||||
|
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
|
||||||
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.103.0" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.103.0", features = ["os"] }
|
nu-engine = { path = "../nu-engine", version = "0.104.1", features = ["os"] }
|
||||||
nu-glob = { path = "../nu-glob", version = "0.103.0" }
|
nu-glob = { path = "../nu-glob", version = "0.104.1" }
|
||||||
nu-path = { path = "../nu-path", version = "0.103.0" }
|
nu-path = { path = "../nu-path", version = "0.104.1" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.103.0", optional = true }
|
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.104.1", optional = true }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", features = ["os"] }
|
nu-protocol = { path = "../nu-protocol", version = "0.104.1", features = ["os"] }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.103.0" }
|
nu-utils = { path = "../nu-utils", version = "0.104.1" }
|
||||||
nu-color-config = { path = "../nu-color-config", version = "0.103.0" }
|
nu-color-config = { path = "../nu-color-config", version = "0.104.1" }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||||
|
|
||||||
|
@ -105,10 +105,9 @@ impl Command for History {
|
|||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
.map(move |entries| {
|
.map(move |entries| {
|
||||||
entries
|
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
||||||
.into_iter()
|
create_sqlite_history_record(idx, entry, long, head)
|
||||||
.enumerate()
|
})
|
||||||
.map(move |(idx, entry)| create_history_record(idx, entry, long, head))
|
|
||||||
})
|
})
|
||||||
.ok_or(IoError::new(
|
.ok_or(IoError::new(
|
||||||
std::io::ErrorKind::NotFound,
|
std::io::ErrorKind::NotFound,
|
||||||
@ -140,7 +139,7 @@ impl Command for History {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) -> Value {
|
fn create_sqlite_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) -> Value {
|
||||||
//1. Format all the values
|
//1. Format all the values
|
||||||
//2. Create a record of either short or long columns and values
|
//2. Create a record of either short or long columns and values
|
||||||
|
|
||||||
@ -151,11 +150,8 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span)
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
head,
|
head,
|
||||||
);
|
);
|
||||||
let start_timestamp_value = Value::string(
|
let start_timestamp_value = Value::date(
|
||||||
entry
|
entry.start_timestamp.unwrap_or_default().fixed_offset(),
|
||||||
.start_timestamp
|
|
||||||
.map(|time| time.to_string())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
head,
|
head,
|
||||||
);
|
);
|
||||||
let command_value = Value::string(entry.command_line, head);
|
let command_value = Value::string(entry.command_line, head);
|
||||||
|
@ -27,7 +27,7 @@ impl Completer for AttributeCompletion {
|
|||||||
let attr_commands =
|
let attr_commands =
|
||||||
working_set.find_commands_by_predicate(|s| s.starts_with(b"attr "), true);
|
working_set.find_commands_by_predicate(|s| s.starts_with(b"attr "), true);
|
||||||
|
|
||||||
for (name, desc, ty) in attr_commands {
|
for (decl_id, name, desc, ty) in attr_commands {
|
||||||
let name = name.strip_prefix(b"attr ").unwrap_or(&name);
|
let name = name.strip_prefix(b"attr ").unwrap_or(&name);
|
||||||
matcher.add_semantic_suggestion(SemanticSuggestion {
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
suggestion: Suggestion {
|
suggestion: Suggestion {
|
||||||
@ -41,7 +41,7 @@ impl Completer for AttributeCompletion {
|
|||||||
},
|
},
|
||||||
append_whitespace: false,
|
append_whitespace: false,
|
||||||
},
|
},
|
||||||
kind: Some(SuggestionKind::Command(ty)),
|
kind: Some(SuggestionKind::Command(ty, Some(decl_id))),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ impl Completer for AttributableCompletion {
|
|||||||
},
|
},
|
||||||
append_whitespace: false,
|
append_whitespace: false,
|
||||||
},
|
},
|
||||||
kind: Some(SuggestionKind::Command(cmd.command_type())),
|
kind: Some(SuggestionKind::Command(cmd.command_type(), None)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::completions::CompletionOptions;
|
use crate::completions::CompletionOptions;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
Span,
|
DeclId, Span,
|
||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ pub struct SemanticSuggestion {
|
|||||||
// TODO: think about name: maybe suggestion context?
|
// TODO: think about name: maybe suggestion context?
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum SuggestionKind {
|
pub enum SuggestionKind {
|
||||||
Command(nu_protocol::engine::CommandType),
|
Command(nu_protocol::engine::CommandType, Option<DeclId>),
|
||||||
Value(nu_protocol::Type),
|
Value(nu_protocol::Type),
|
||||||
CellPath,
|
CellPath,
|
||||||
Directory,
|
Directory,
|
||||||
|
@ -17,14 +17,14 @@ pub struct CellPathCompletion<'a> {
|
|||||||
|
|
||||||
fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) {
|
fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) {
|
||||||
let (prefix_str, start) = match member {
|
let (prefix_str, start) = match member {
|
||||||
PathMember::String { val, span, .. } => (val.clone(), span.start),
|
PathMember::String { val, span, .. } => (val, span.start),
|
||||||
PathMember::Int { val, span, .. } => (val.to_string(), span.start),
|
PathMember::Int { val, span, .. } => (&val.to_string(), span.start),
|
||||||
};
|
};
|
||||||
let prefix_str = prefix_str
|
let prefix_str = prefix_str.get(..pos + 1 - start).unwrap_or(prefix_str);
|
||||||
.get(..pos + 1 - start)
|
// strip wrapping quotes
|
||||||
.map(str::to_string)
|
let quotations = ['"', '\'', '`'];
|
||||||
.unwrap_or(prefix_str);
|
let prefix_str = prefix_str.strip_prefix(quotations).unwrap_or(prefix_str);
|
||||||
(prefix_str, Span::new(start, pos + 1))
|
(prefix_str.to_string(), Span::new(start, pos + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for CellPathCompletion<'_> {
|
impl Completer for CellPathCompletion<'_> {
|
||||||
@ -108,14 +108,26 @@ fn get_suggestions_by_value(
|
|||||||
value: &Value,
|
value: &Value,
|
||||||
current_span: reedline::Span,
|
current_span: reedline::Span,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
let to_suggestion = |s: String, v: Option<&Value>| SemanticSuggestion {
|
let to_suggestion = |s: String, v: Option<&Value>| {
|
||||||
suggestion: Suggestion {
|
// Check if the string needs quoting
|
||||||
value: s,
|
let value = if s.is_empty()
|
||||||
span: current_span,
|
|| s.chars()
|
||||||
description: v.map(|v| v.get_type().to_string()),
|
.any(|c: char| !(c.is_ascii_alphabetic() || ['_', '-'].contains(&c)))
|
||||||
..Suggestion::default()
|
{
|
||||||
},
|
format!("{:?}", s)
|
||||||
kind: Some(SuggestionKind::CellPath),
|
} else {
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
span: current_span,
|
||||||
|
description: v.map(|v| v.get_type().to_string()),
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::CellPath),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match value {
|
match value {
|
||||||
Value::Record { val, .. } => val
|
Value::Record { val, .. } => val
|
||||||
|
@ -75,7 +75,10 @@ impl CommandCompletion {
|
|||||||
append_whitespace: true,
|
append_whitespace: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
kind: Some(SuggestionKind::Command(CommandType::External)),
|
kind: Some(SuggestionKind::Command(
|
||||||
|
CommandType::External,
|
||||||
|
None,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -112,7 +115,7 @@ impl Completer for CommandCompletion {
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
for (name, description, typ) in filtered_commands {
|
for (decl_id, name, description, typ) in filtered_commands {
|
||||||
let name = String::from_utf8_lossy(&name);
|
let name = String::from_utf8_lossy(&name);
|
||||||
internal_suggs.insert(
|
internal_suggs.insert(
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
@ -124,7 +127,7 @@ impl Completer for CommandCompletion {
|
|||||||
append_whitespace: true,
|
append_whitespace: true,
|
||||||
..Suggestion::default()
|
..Suggestion::default()
|
||||||
},
|
},
|
||||||
kind: Some(SuggestionKind::Command(typ)),
|
kind: Some(SuggestionKind::Command(typ, Some(decl_id))),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
use crate::completions::{
|
use crate::completions::{
|
||||||
|
base::{SemanticSuggestion, SuggestionKind},
|
||||||
AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
|
AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
|
||||||
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion,
|
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion,
|
||||||
FlagCompletion, OperatorCompletion, VariableCompletion,
|
ExportableCompletion, 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;
|
||||||
use nu_parser::{flatten_expression, parse};
|
use nu_parser::{flatten_expression, parse, parse_module_file_or_dir};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Argument, Block, Expr, Expression, FindMapResult, Traverse},
|
ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse},
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
||||||
PipelineData, Span, Type, Value,
|
PipelineData, Span, Type, Value,
|
||||||
};
|
};
|
||||||
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
||||||
use std::{str, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::base::{SemanticSuggestion, SuggestionKind};
|
|
||||||
|
|
||||||
/// Used as the function `f` in find_map Traverse
|
/// Used as the function `f` in find_map Traverse
|
||||||
///
|
///
|
||||||
@ -57,8 +56,13 @@ fn find_pipeline_element_by_position<'a>(
|
|||||||
Expr::FullCellPath(fcp) => fcp
|
Expr::FullCellPath(fcp) => fcp
|
||||||
.head
|
.head
|
||||||
.find_map(working_set, &closure)
|
.find_map(working_set, &closure)
|
||||||
.or(Some(expr))
|
|
||||||
.map(FindMapResult::Found)
|
.map(FindMapResult::Found)
|
||||||
|
// e.g. use std/util [<tab>
|
||||||
|
.or_else(|| {
|
||||||
|
(fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
|
||||||
|
.then_some(FindMapResult::Continue)
|
||||||
|
})
|
||||||
|
.or(Some(FindMapResult::Found(expr)))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
Expr::Var(_) => FindMapResult::Found(expr),
|
Expr::Var(_) => FindMapResult::Found(expr),
|
||||||
Expr::AttributeBlock(ab) => ab
|
Expr::AttributeBlock(ab) => ab
|
||||||
@ -127,6 +131,18 @@ struct Context<'a> {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For argument completion
|
||||||
|
struct PositionalArguments<'a> {
|
||||||
|
/// command name
|
||||||
|
command_head: &'a str,
|
||||||
|
/// indices of positional arguments
|
||||||
|
positional_arg_indices: Vec<usize>,
|
||||||
|
/// argument list
|
||||||
|
arguments: &'a [Argument],
|
||||||
|
/// expression of current argument
|
||||||
|
expr: &'a Expression,
|
||||||
|
}
|
||||||
|
|
||||||
impl Context<'_> {
|
impl Context<'_> {
|
||||||
fn new<'a>(
|
fn new<'a>(
|
||||||
working_set: &'a StateWorkingSet,
|
working_set: &'a StateWorkingSet,
|
||||||
@ -328,7 +344,8 @@ impl NuCompleter {
|
|||||||
// NOTE: the argument to complete is not necessarily the last one
|
// NOTE: the argument to complete is not necessarily the last one
|
||||||
// for lsp completion, we don't trim the text,
|
// for lsp completion, we don't trim the text,
|
||||||
// so that `def`s after pos can be completed
|
// so that `def`s after pos can be completed
|
||||||
for arg in call.arguments.iter() {
|
let mut positional_arg_indices = Vec::new();
|
||||||
|
for (arg_idx, arg) in call.arguments.iter().enumerate() {
|
||||||
let span = arg.span();
|
let span = arg.span();
|
||||||
if span.contains(pos) {
|
if span.contains(pos) {
|
||||||
// if customized completion specified, it has highest priority
|
// if customized completion specified, it has highest priority
|
||||||
@ -378,10 +395,16 @@ impl NuCompleter {
|
|||||||
Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
|
Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
|
||||||
// complete according to expression type and command head
|
// complete according to expression type and command head
|
||||||
Argument::Positional(expr) => {
|
Argument::Positional(expr) => {
|
||||||
let command_head = working_set.get_span_contents(call.head);
|
let command_head = working_set.get_decl(call.decl_id).name();
|
||||||
|
positional_arg_indices.push(arg_idx);
|
||||||
self.argument_completion_helper(
|
self.argument_completion_helper(
|
||||||
command_head,
|
PositionalArguments {
|
||||||
expr,
|
command_head,
|
||||||
|
positional_arg_indices,
|
||||||
|
arguments: &call.arguments,
|
||||||
|
expr,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
&ctx,
|
&ctx,
|
||||||
suggestions.is_empty(),
|
suggestions.is_empty(),
|
||||||
)
|
)
|
||||||
@ -389,6 +412,8 @@ impl NuCompleter {
|
|||||||
_ => vec![],
|
_ => vec![],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
} else if !matches!(arg, Argument::Named(_)) {
|
||||||
|
positional_arg_indices.push(arg_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -486,9 +511,10 @@ impl NuCompleter {
|
|||||||
externals: bool,
|
externals: bool,
|
||||||
strip: bool,
|
strip: bool,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let config = self.engine_state.get_config();
|
||||||
let mut command_completions = CommandCompletion {
|
let mut command_completions = CommandCompletion {
|
||||||
internals,
|
internals,
|
||||||
externals,
|
externals: !internals || (externals && config.completions.external.enable),
|
||||||
};
|
};
|
||||||
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
@ -497,20 +523,97 @@ impl NuCompleter {
|
|||||||
|
|
||||||
fn argument_completion_helper(
|
fn argument_completion_helper(
|
||||||
&self,
|
&self,
|
||||||
command_head: &[u8],
|
argument_info: PositionalArguments,
|
||||||
expr: &Expression,
|
pos: usize,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
need_fallback: bool,
|
need_fallback: bool,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let PositionalArguments {
|
||||||
|
command_head,
|
||||||
|
positional_arg_indices,
|
||||||
|
arguments,
|
||||||
|
expr,
|
||||||
|
} = argument_info;
|
||||||
// special commands
|
// special commands
|
||||||
match command_head {
|
match command_head {
|
||||||
// complete module file/directory
|
// complete module file/directory
|
||||||
// TODO: if module file already specified,
|
"use" | "export use" | "overlay use" | "source-env"
|
||||||
// should parse it to get modules/commands/consts to complete
|
if positional_arg_indices.len() == 1 =>
|
||||||
b"use" | b"export use" | b"overlay use" | b"source-env" => {
|
{
|
||||||
return self.process_completion(&mut DotNuCompletion, ctx);
|
return self.process_completion(
|
||||||
|
&mut DotNuCompletion {
|
||||||
|
std_virtual_path: command_head != "source-env",
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
b"which" => {
|
// NOTE: if module file already specified,
|
||||||
|
// should parse it to get modules/commands/consts to complete
|
||||||
|
"use" | "export use" => {
|
||||||
|
let Some(Argument::Positional(Expression {
|
||||||
|
expr: Expr::String(module_name),
|
||||||
|
span,
|
||||||
|
..
|
||||||
|
})) = positional_arg_indices
|
||||||
|
.first()
|
||||||
|
.and_then(|i| arguments.get(*i))
|
||||||
|
else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let module_name = module_name.as_bytes();
|
||||||
|
let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) {
|
||||||
|
Some(module_id) => (module_id, None),
|
||||||
|
None => {
|
||||||
|
let mut temp_working_set =
|
||||||
|
StateWorkingSet::new(ctx.working_set.permanent_state);
|
||||||
|
let Some(module_id) = parse_module_file_or_dir(
|
||||||
|
&mut temp_working_set,
|
||||||
|
module_name,
|
||||||
|
*span,
|
||||||
|
None,
|
||||||
|
) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
(module_id, Some(temp_working_set))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut exportable_completion = ExportableCompletion {
|
||||||
|
module_id,
|
||||||
|
temp_working_set,
|
||||||
|
};
|
||||||
|
let mut complete_on_list_items = |items: &[ListItem]| -> Vec<SemanticSuggestion> {
|
||||||
|
for item in items {
|
||||||
|
let span = item.expr().span;
|
||||||
|
if span.contains(pos) {
|
||||||
|
let offset = span.start.saturating_sub(ctx.span.start);
|
||||||
|
let end_offset =
|
||||||
|
ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1);
|
||||||
|
let new_ctx = Context::new(
|
||||||
|
ctx.working_set,
|
||||||
|
Span::new(span.start, ctx.span.end.min(span.end)),
|
||||||
|
ctx.prefix.get(offset..end_offset).unwrap_or_default(),
|
||||||
|
ctx.offset,
|
||||||
|
);
|
||||||
|
return self.process_completion(&mut exportable_completion, &new_ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
match &expr.expr {
|
||||||
|
Expr::String(_) => {
|
||||||
|
return self.process_completion(&mut exportable_completion, ctx);
|
||||||
|
}
|
||||||
|
Expr::FullCellPath(fcp) => match &fcp.head.expr {
|
||||||
|
Expr::List(items) => {
|
||||||
|
return complete_on_list_items(items);
|
||||||
|
}
|
||||||
|
_ => return vec![],
|
||||||
|
},
|
||||||
|
_ => return vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"which" => {
|
||||||
let mut completer = CommandCompletion {
|
let mut completer = CommandCompletion {
|
||||||
internals: true,
|
internals: true,
|
||||||
externals: true,
|
externals: true,
|
||||||
@ -543,7 +646,6 @@ impl NuCompleter {
|
|||||||
case_sensitive: config.completions.case_sensitive,
|
case_sensitive: config.completions.case_sensitive,
|
||||||
match_algorithm: config.completions.algorithm.into(),
|
match_algorithm: config.completions.algorithm.into(),
|
||||||
sort: config.completions.sort,
|
sort: config.completions.sort,
|
||||||
..Default::default()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
completer.fetch(
|
completer.fetch(
|
||||||
|
@ -22,18 +22,22 @@ pub struct PathBuiltFromString {
|
|||||||
/// Recursively goes through paths that match a given `partial`.
|
/// Recursively goes through paths that match a given `partial`.
|
||||||
/// built: State struct for a valid matching path built so far.
|
/// built: State struct for a valid matching path built so far.
|
||||||
///
|
///
|
||||||
|
/// `want_directory`: Whether we want only directories as completion matches.
|
||||||
|
/// Some commands like `cd` can only be run on directories whereas others
|
||||||
|
/// like `ls` can be run on regular files as well.
|
||||||
|
///
|
||||||
/// `isdir`: whether the current partial path has a trailing slash.
|
/// `isdir`: whether the current partial path has a trailing slash.
|
||||||
/// Parsing a path string into a pathbuf loses that bit of information.
|
/// Parsing a path string into a pathbuf loses that bit of information.
|
||||||
///
|
///
|
||||||
/// want_directory: Whether we want only directories as completion matches.
|
/// `enable_exact_match`: Whether match algorithm is Prefix and all previous components
|
||||||
/// Some commands like `cd` can only be run on directories whereas others
|
/// of the path matched a directory exactly.
|
||||||
/// like `ls` can be run on regular files as well.
|
|
||||||
fn complete_rec(
|
fn complete_rec(
|
||||||
partial: &[&str],
|
partial: &[&str],
|
||||||
built_paths: &[PathBuiltFromString],
|
built_paths: &[PathBuiltFromString],
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
want_directory: bool,
|
want_directory: bool,
|
||||||
isdir: bool,
|
isdir: bool,
|
||||||
|
enable_exact_match: bool,
|
||||||
) -> Vec<PathBuiltFromString> {
|
) -> Vec<PathBuiltFromString> {
|
||||||
if let Some((&base, rest)) = partial.split_first() {
|
if let Some((&base, rest)) = partial.split_first() {
|
||||||
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
||||||
@ -46,7 +50,14 @@ fn complete_rec(
|
|||||||
built
|
built
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
return complete_rec(rest, &built_paths, options, want_directory, isdir);
|
return complete_rec(
|
||||||
|
rest,
|
||||||
|
&built_paths,
|
||||||
|
options,
|
||||||
|
want_directory,
|
||||||
|
isdir,
|
||||||
|
enable_exact_match,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,27 +97,26 @@ fn complete_rec(
|
|||||||
// Serves as confirmation to ignore longer completions for
|
// Serves as confirmation to ignore longer completions for
|
||||||
// components in between.
|
// components in between.
|
||||||
if !rest.is_empty() || isdir {
|
if !rest.is_empty() || isdir {
|
||||||
|
// Don't show longer completions if we have an exact match (#13204, #14794)
|
||||||
|
let exact_match = enable_exact_match
|
||||||
|
&& (if options.case_sensitive {
|
||||||
|
entry_name.eq(base)
|
||||||
|
} else {
|
||||||
|
entry_name.eq_ignore_case(base)
|
||||||
|
});
|
||||||
completions.extend(complete_rec(
|
completions.extend(complete_rec(
|
||||||
rest,
|
rest,
|
||||||
&[built],
|
&[built],
|
||||||
options,
|
options,
|
||||||
want_directory,
|
want_directory,
|
||||||
isdir,
|
isdir,
|
||||||
|
exact_match,
|
||||||
));
|
));
|
||||||
} else {
|
|
||||||
completions.push(built);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For https://github.com/nushell/nushell/issues/13204
|
|
||||||
if isdir && options.match_algorithm == MatchAlgorithm::Prefix {
|
|
||||||
let exact_match = if options.case_sensitive {
|
|
||||||
entry_name.eq(base)
|
|
||||||
} else {
|
|
||||||
entry_name.to_folded_case().eq(&base.to_folded_case())
|
|
||||||
};
|
|
||||||
if exact_match {
|
if exact_match {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
completions.push(built);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@ -140,7 +150,7 @@ impl OriginalCwd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn surround_remove(partial: &str) -> String {
|
pub fn surround_remove(partial: &str) -> String {
|
||||||
for c in ['`', '"', '\''] {
|
for c in ['`', '"', '\''] {
|
||||||
if partial.starts_with(c) {
|
if partial.starts_with(c) {
|
||||||
let ret = partial.strip_prefix(c).unwrap_or(partial);
|
let ret = partial.strip_prefix(c).unwrap_or(partial);
|
||||||
@ -199,10 +209,9 @@ pub fn complete_item(
|
|||||||
let ls_colors = (engine_state.config.completions.use_ls_colors
|
let ls_colors = (engine_state.config.completions.use_ls_colors
|
||||||
&& engine_state.config.use_ansi_coloring.get(engine_state))
|
&& engine_state.config.use_ansi_coloring.get(engine_state))
|
||||||
.then(|| {
|
.then(|| {
|
||||||
let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
|
let ls_colors_env_str = stack
|
||||||
Some(v) => env_to_string("LS_COLORS", v, engine_state, stack).ok(),
|
.get_env_var(engine_state, "LS_COLORS")
|
||||||
None => None,
|
.and_then(|v| env_to_string("LS_COLORS", v, engine_state, stack).ok());
|
||||||
};
|
|
||||||
get_ls_colors(ls_colors_env_str)
|
get_ls_colors(ls_colors_env_str)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -256,6 +265,7 @@ pub fn complete_item(
|
|||||||
options,
|
options,
|
||||||
want_directory,
|
want_directory,
|
||||||
isdir,
|
isdir,
|
||||||
|
options.match_algorithm == MatchAlgorithm::Prefix,
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mut p| {
|
.map(|mut p| {
|
||||||
@ -264,15 +274,12 @@ pub fn complete_item(
|
|||||||
}
|
}
|
||||||
let is_dir = p.isdir;
|
let is_dir = p.isdir;
|
||||||
let path = original_cwd.apply(p, path_separator);
|
let path = original_cwd.apply(p, path_separator);
|
||||||
|
let real_path = expand_to_real_path(&path);
|
||||||
|
let metadata = std::fs::symlink_metadata(&real_path).ok();
|
||||||
let style = ls_colors.as_ref().map(|lsc| {
|
let style = ls_colors.as_ref().map(|lsc| {
|
||||||
lsc.style_for_path_with_metadata(
|
lsc.style_for_path_with_metadata(&real_path, metadata.as_ref())
|
||||||
&path,
|
.map(lscolors::Style::to_nu_ansi_term_style)
|
||||||
std::fs::symlink_metadata(expand_to_real_path(&path))
|
.unwrap_or_default()
|
||||||
.ok()
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.map(lscolors::Style::to_nu_ansi_term_style)
|
|
||||||
.unwrap_or_default()
|
|
||||||
});
|
});
|
||||||
FileSuggestion {
|
FileSuggestion {
|
||||||
span,
|
span,
|
||||||
|
@ -18,6 +18,12 @@ pub enum MatchAlgorithm {
|
|||||||
/// "git switch" is matched by "git sw"
|
/// "git switch" is matched by "git sw"
|
||||||
Prefix,
|
Prefix,
|
||||||
|
|
||||||
|
/// Only show suggestions which have a substring matching with the given input
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "git checkout" is matched by "checkout"
|
||||||
|
Substring,
|
||||||
|
|
||||||
/// Only show suggestions which contain the input chars at any place
|
/// Only show suggestions which contain the input chars at any place
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
@ -36,6 +42,10 @@ enum State<T> {
|
|||||||
/// Holds (haystack, item)
|
/// Holds (haystack, item)
|
||||||
items: Vec<(String, T)>,
|
items: Vec<(String, T)>,
|
||||||
},
|
},
|
||||||
|
Substring {
|
||||||
|
/// Holds (haystack, item)
|
||||||
|
items: Vec<(String, T)>,
|
||||||
|
},
|
||||||
Fuzzy {
|
Fuzzy {
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
atom: Atom,
|
atom: Atom,
|
||||||
@ -64,6 +74,18 @@ impl<T> NuMatcher<'_, T> {
|
|||||||
state: State::Prefix { items: Vec::new() },
|
state: State::Prefix { items: Vec::new() },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MatchAlgorithm::Substring => {
|
||||||
|
let lowercase_needle = if options.case_sensitive {
|
||||||
|
needle.to_owned()
|
||||||
|
} else {
|
||||||
|
needle.to_folded_case()
|
||||||
|
};
|
||||||
|
NuMatcher {
|
||||||
|
options,
|
||||||
|
needle: lowercase_needle,
|
||||||
|
state: State::Substring { items: Vec::new() },
|
||||||
|
}
|
||||||
|
}
|
||||||
MatchAlgorithm::Fuzzy => {
|
MatchAlgorithm::Fuzzy => {
|
||||||
let atom = Atom::new(
|
let atom = Atom::new(
|
||||||
needle,
|
needle,
|
||||||
@ -102,11 +124,21 @@ impl<T> NuMatcher<'_, T> {
|
|||||||
} else {
|
} else {
|
||||||
Cow::Owned(haystack.to_folded_case())
|
Cow::Owned(haystack.to_folded_case())
|
||||||
};
|
};
|
||||||
let matches = if self.options.positional {
|
let matches = haystack_folded.starts_with(self.needle.as_str());
|
||||||
haystack_folded.starts_with(self.needle.as_str())
|
if matches {
|
||||||
|
if let Some(item) = item {
|
||||||
|
items.push((haystack.to_string(), item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches
|
||||||
|
}
|
||||||
|
State::Substring { items } => {
|
||||||
|
let haystack_folded = if self.options.case_sensitive {
|
||||||
|
Cow::Borrowed(haystack)
|
||||||
} else {
|
} else {
|
||||||
haystack_folded.contains(self.needle.as_str())
|
Cow::Owned(haystack.to_folded_case())
|
||||||
};
|
};
|
||||||
|
let matches = haystack_folded.contains(self.needle.as_str());
|
||||||
if matches {
|
if matches {
|
||||||
if let Some(item) = item {
|
if let Some(item) = item {
|
||||||
items.push((haystack.to_string(), item));
|
items.push((haystack.to_string(), item));
|
||||||
@ -148,7 +180,7 @@ impl<T> NuMatcher<'_, T> {
|
|||||||
/// Get all the items that matched (sorted)
|
/// Get all the items that matched (sorted)
|
||||||
pub fn results(self) -> Vec<T> {
|
pub fn results(self) -> Vec<T> {
|
||||||
match self.state {
|
match self.state {
|
||||||
State::Prefix { mut items, .. } => {
|
State::Prefix { mut items, .. } | State::Substring { mut items, .. } => {
|
||||||
items.sort_by(|(haystack1, _), (haystack2, _)| {
|
items.sort_by(|(haystack1, _), (haystack2, _)| {
|
||||||
let cmp_sensitive = haystack1.cmp(haystack2);
|
let cmp_sensitive = haystack1.cmp(haystack2);
|
||||||
if self.options.case_sensitive {
|
if self.options.case_sensitive {
|
||||||
@ -195,6 +227,7 @@ impl From<CompletionAlgorithm> for MatchAlgorithm {
|
|||||||
fn from(value: CompletionAlgorithm) -> Self {
|
fn from(value: CompletionAlgorithm) -> Self {
|
||||||
match value {
|
match value {
|
||||||
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
|
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
|
||||||
|
CompletionAlgorithm::Substring => MatchAlgorithm::Substring,
|
||||||
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
|
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,6 +239,7 @@ impl TryFrom<String> for MatchAlgorithm {
|
|||||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
match value.as_str() {
|
match value.as_str() {
|
||||||
"prefix" => Ok(Self::Prefix),
|
"prefix" => Ok(Self::Prefix),
|
||||||
|
"substring" => Ok(Self::Substring),
|
||||||
"fuzzy" => Ok(Self::Fuzzy),
|
"fuzzy" => Ok(Self::Fuzzy),
|
||||||
_ => Err(InvalidMatchAlgorithm::Unknown),
|
_ => Err(InvalidMatchAlgorithm::Unknown),
|
||||||
}
|
}
|
||||||
@ -230,7 +264,6 @@ impl std::error::Error for InvalidMatchAlgorithm {}
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CompletionOptions {
|
pub struct CompletionOptions {
|
||||||
pub case_sensitive: bool,
|
pub case_sensitive: bool,
|
||||||
pub positional: bool,
|
|
||||||
pub match_algorithm: MatchAlgorithm,
|
pub match_algorithm: MatchAlgorithm,
|
||||||
pub sort: CompletionSort,
|
pub sort: CompletionSort,
|
||||||
}
|
}
|
||||||
@ -239,7 +272,6 @@ impl Default for CompletionOptions {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
case_sensitive: true,
|
case_sensitive: true,
|
||||||
positional: true,
|
|
||||||
match_algorithm: MatchAlgorithm::Prefix,
|
match_algorithm: MatchAlgorithm::Prefix,
|
||||||
sort: Default::default(),
|
sort: Default::default(),
|
||||||
}
|
}
|
||||||
@ -256,6 +288,9 @@ mod test {
|
|||||||
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
||||||
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
||||||
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
|
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
|
||||||
|
#[case(MatchAlgorithm::Substring, "example text", "", true)]
|
||||||
|
#[case(MatchAlgorithm::Substring, "example text", "text", true)]
|
||||||
|
#[case(MatchAlgorithm::Substring, "example text", "mplxt", false)]
|
||||||
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
|
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
|
||||||
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
||||||
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
|
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use crate::completions::{
|
use crate::completions::{
|
||||||
completer::map_value_completions, Completer, CompletionOptions, SemanticSuggestion,
|
completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm,
|
||||||
|
SemanticSuggestion,
|
||||||
};
|
};
|
||||||
use nu_engine::eval_call;
|
use nu_engine::eval_call;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Argument, Call, Expr, Expression},
|
ast::{Argument, Call, Expr, Expression},
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
DeclId, PipelineData, Span, Type, Value,
|
DeclId, PipelineData, Span, Type, Value,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -42,28 +43,37 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
|||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
// Call custom declaration
|
// Call custom declaration
|
||||||
let mut stack_mut = stack.clone();
|
let mut stack_mut = stack.clone();
|
||||||
let result = eval_call::<WithoutDebug>(
|
let mut eval = |engine_state: &EngineState| {
|
||||||
working_set.permanent_state,
|
eval_call::<WithoutDebug>(
|
||||||
&mut stack_mut,
|
engine_state,
|
||||||
&Call {
|
&mut stack_mut,
|
||||||
decl_id: self.decl_id,
|
&Call {
|
||||||
head: span,
|
decl_id: self.decl_id,
|
||||||
arguments: vec![
|
head: span,
|
||||||
Argument::Positional(Expression::new_unknown(
|
arguments: vec![
|
||||||
Expr::String(self.line.clone()),
|
Argument::Positional(Expression::new_unknown(
|
||||||
Span::unknown(),
|
Expr::String(self.line.clone()),
|
||||||
Type::String,
|
Span::unknown(),
|
||||||
)),
|
Type::String,
|
||||||
Argument::Positional(Expression::new_unknown(
|
)),
|
||||||
Expr::Int(self.line_pos as i64),
|
Argument::Positional(Expression::new_unknown(
|
||||||
Span::unknown(),
|
Expr::Int(self.line_pos as i64),
|
||||||
Type::Int,
|
Span::unknown(),
|
||||||
)),
|
Type::Int,
|
||||||
],
|
)),
|
||||||
parser_info: HashMap::new(),
|
],
|
||||||
},
|
parser_info: HashMap::new(),
|
||||||
PipelineData::empty(),
|
},
|
||||||
);
|
PipelineData::empty(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let result = if self.decl_id.get() < working_set.permanent_state.num_decls() {
|
||||||
|
eval(working_set.permanent_state)
|
||||||
|
} else {
|
||||||
|
let mut engine_state = working_set.permanent_state.clone();
|
||||||
|
let _ = engine_state.merge_delta(working_set.delta.clone());
|
||||||
|
eval(&engine_state)
|
||||||
|
};
|
||||||
|
|
||||||
let mut completion_options = orig_options.clone();
|
let mut completion_options = orig_options.clone();
|
||||||
let mut should_sort = true;
|
let mut should_sort = true;
|
||||||
@ -93,10 +103,10 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
|||||||
{
|
{
|
||||||
completion_options.case_sensitive = case_sensitive;
|
completion_options.case_sensitive = case_sensitive;
|
||||||
}
|
}
|
||||||
if let Some(positional) =
|
let positional =
|
||||||
options.get("positional").and_then(|val| val.as_bool().ok())
|
options.get("positional").and_then(|val| val.as_bool().ok());
|
||||||
{
|
if positional.is_some() {
|
||||||
completion_options.positional = positional;
|
log::warn!("Use of the positional option is deprecated. Use the substring match algorithm instead.");
|
||||||
}
|
}
|
||||||
if let Some(algorithm) = options
|
if let Some(algorithm) = options
|
||||||
.get("completion_algorithm")
|
.get("completion_algorithm")
|
||||||
@ -104,6 +114,11 @@ impl<T: Completer> Completer for CustomCompletion<T> {
|
|||||||
.and_then(|option| option.try_into().ok())
|
.and_then(|option| option.try_into().ok())
|
||||||
{
|
{
|
||||||
completion_options.match_algorithm = algorithm;
|
completion_options.match_algorithm = algorithm;
|
||||||
|
if let Some(false) = positional {
|
||||||
|
if completion_options.match_algorithm == MatchAlgorithm::Prefix {
|
||||||
|
completion_options.match_algorithm = MatchAlgorithm::Substring
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
use crate::completions::{file_path_completion, Completer, CompletionOptions};
|
use crate::completions::{
|
||||||
|
completion_common::{surround_remove, FileSuggestion},
|
||||||
|
completion_options::NuMatcher,
|
||||||
|
file_path_completion, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||||
|
};
|
||||||
use nu_path::expand_tilde;
|
use nu_path::expand_tilde;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet, VirtualPath},
|
||||||
Span,
|
Span,
|
||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR},
|
path::{is_separator, PathBuf, MAIN_SEPARATOR_STR},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{SemanticSuggestion, SuggestionKind};
|
pub struct DotNuCompletion {
|
||||||
|
/// e.g. use std/a<tab>
|
||||||
pub struct DotNuCompletion;
|
pub std_virtual_path: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Completer for DotNuCompletion {
|
impl Completer for DotNuCompletion {
|
||||||
fn fetch(
|
fn fetch(
|
||||||
@ -102,7 +107,7 @@ impl Completer for DotNuCompletion {
|
|||||||
|
|
||||||
// Fetch the files filtering the ones that ends with .nu
|
// Fetch the files filtering the ones that ends with .nu
|
||||||
// and transform them into suggestions
|
// and transform them into suggestions
|
||||||
let completions = file_path_completion(
|
let mut completions = file_path_completion(
|
||||||
span,
|
span,
|
||||||
partial,
|
partial,
|
||||||
&search_dirs
|
&search_dirs
|
||||||
@ -113,17 +118,60 @@ impl Completer for DotNuCompletion {
|
|||||||
working_set.permanent_state,
|
working_set.permanent_state,
|
||||||
stack,
|
stack,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if self.std_virtual_path {
|
||||||
|
let mut matcher = NuMatcher::new(partial, options);
|
||||||
|
let base_dir = surround_remove(&base_dir);
|
||||||
|
if base_dir == "." {
|
||||||
|
let surround_prefix = partial
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| "`'\"".contains(*c))
|
||||||
|
.collect::<String>();
|
||||||
|
for path in ["std", "std-rfc"] {
|
||||||
|
let path = format!("{}{}", surround_prefix, path);
|
||||||
|
matcher.add(
|
||||||
|
path.clone(),
|
||||||
|
FileSuggestion {
|
||||||
|
span,
|
||||||
|
path,
|
||||||
|
style: None,
|
||||||
|
is_dir: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if let Some(VirtualPath::Dir(sub_paths)) =
|
||||||
|
working_set.find_virtual_path(&base_dir)
|
||||||
|
{
|
||||||
|
for sub_vp_id in sub_paths {
|
||||||
|
let (path, sub_vp) = working_set.get_virtual_path(*sub_vp_id);
|
||||||
|
let path = path
|
||||||
|
.strip_prefix(&format!("{}/", base_dir))
|
||||||
|
.unwrap_or(path)
|
||||||
|
.to_string();
|
||||||
|
matcher.add(
|
||||||
|
path.clone(),
|
||||||
|
FileSuggestion {
|
||||||
|
path,
|
||||||
|
span,
|
||||||
|
style: None,
|
||||||
|
is_dir: matches!(sub_vp, VirtualPath::Dir(_)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completions.extend(matcher.results());
|
||||||
|
}
|
||||||
|
|
||||||
completions
|
completions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// Different base dir, so we list the .nu files or folders
|
// Different base dir, so we list the .nu files or folders
|
||||||
.filter(|it| {
|
.filter(|it| {
|
||||||
// for paths with spaces in them
|
// for paths with spaces in them
|
||||||
let path = it.path.trim_end_matches('`');
|
let path = it.path.trim_end_matches('`');
|
||||||
path.ends_with(".nu") || path.ends_with(SEP)
|
path.ends_with(".nu") || it.is_dir
|
||||||
})
|
})
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
let append_whitespace =
|
let append_whitespace = !x.is_dir && (!start_with_backquote || end_with_backquote);
|
||||||
x.path.ends_with(".nu") && (!start_with_backquote || end_with_backquote);
|
|
||||||
// Re-calculate the span to replace
|
// Re-calculate the span to replace
|
||||||
let mut span_offset = 0;
|
let mut span_offset = 0;
|
||||||
let mut value = x.path.to_string();
|
let mut value = x.path.to_string();
|
||||||
|
112
crates/nu-cli/src/completions/exportable_completions.rs
Normal file
112
crates/nu-cli/src/completions/exportable_completions.rs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
completion_common::surround_remove, completion_options::NuMatcher, Completer,
|
||||||
|
CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
ModuleId, Span,
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
pub struct ExportableCompletion<'a> {
|
||||||
|
pub module_id: ModuleId,
|
||||||
|
pub temp_working_set: Option<StateWorkingSet<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If name contains space, wrap it in quotes
|
||||||
|
fn wrapped_name(name: String) -> String {
|
||||||
|
if !name.contains(' ') {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if name.contains('\'') {
|
||||||
|
format!("\"{}\"", name.replace('"', r#"\""#))
|
||||||
|
} else {
|
||||||
|
format!("'{name}'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for ExportableCompletion<'_> {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
_stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut matcher = NuMatcher::<()>::new(surround_remove(prefix.as_ref()), options);
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let span = reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
};
|
||||||
|
// TODO: use matcher.add_lazy to lazy evaluate an item if it matches the prefix
|
||||||
|
let mut add_suggestion = |value: String,
|
||||||
|
description: Option<String>,
|
||||||
|
extra: Option<Vec<String>>,
|
||||||
|
kind: SuggestionKind| {
|
||||||
|
results.push(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
span,
|
||||||
|
description,
|
||||||
|
extra,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(kind),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let working_set = self.temp_working_set.as_ref().unwrap_or(working_set);
|
||||||
|
let module = working_set.get_module(self.module_id);
|
||||||
|
|
||||||
|
for (name, decl_id) in &module.decls {
|
||||||
|
let name = String::from_utf8_lossy(name).to_string();
|
||||||
|
if matcher.matches(&name) {
|
||||||
|
let cmd = working_set.get_decl(*decl_id);
|
||||||
|
add_suggestion(
|
||||||
|
wrapped_name(name),
|
||||||
|
Some(cmd.description().to_string()),
|
||||||
|
None,
|
||||||
|
// `None` here avoids arguments being expanded by snippet edit style for lsp
|
||||||
|
SuggestionKind::Command(cmd.command_type(), None),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (name, module_id) in &module.submodules {
|
||||||
|
let name = String::from_utf8_lossy(name).to_string();
|
||||||
|
if matcher.matches(&name) {
|
||||||
|
let comments = working_set.get_module_comments(*module_id).map(|spans| {
|
||||||
|
spans
|
||||||
|
.iter()
|
||||||
|
.map(|sp| {
|
||||||
|
String::from_utf8_lossy(working_set.get_span_contents(*sp)).into()
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
});
|
||||||
|
add_suggestion(
|
||||||
|
wrapped_name(name),
|
||||||
|
Some("Submodule".into()),
|
||||||
|
comments,
|
||||||
|
SuggestionKind::Module,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (name, var_id) in &module.constants {
|
||||||
|
let name = String::from_utf8_lossy(name).to_string();
|
||||||
|
if matcher.matches(&name) {
|
||||||
|
let var = working_set.get_variable(*var_id);
|
||||||
|
add_suggestion(
|
||||||
|
wrapped_name(name),
|
||||||
|
var.const_val
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v.clone().coerce_into_string().ok()),
|
||||||
|
None,
|
||||||
|
SuggestionKind::Variable,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions};
|
use crate::completions::{
|
||||||
|
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||||
|
};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
DeclId, Span,
|
DeclId, Span,
|
||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
|
|
||||||
use super::{SemanticSuggestion, SuggestionKind};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FlagCompletion {
|
pub struct FlagCompletion {
|
||||||
pub decl_id: DeclId,
|
pub decl_id: DeclId,
|
||||||
|
@ -8,6 +8,7 @@ mod completion_options;
|
|||||||
mod custom_completions;
|
mod custom_completions;
|
||||||
mod directory_completions;
|
mod directory_completions;
|
||||||
mod dotnu_completions;
|
mod dotnu_completions;
|
||||||
|
mod exportable_completions;
|
||||||
mod file_completions;
|
mod file_completions;
|
||||||
mod flag_completions;
|
mod flag_completions;
|
||||||
mod operator_completions;
|
mod operator_completions;
|
||||||
@ -22,6 +23,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
|
|||||||
pub use custom_completions::CustomCompletion;
|
pub use custom_completions::CustomCompletion;
|
||||||
pub use directory_completions::DirectoryCompletion;
|
pub use directory_completions::DirectoryCompletion;
|
||||||
pub use dotnu_completions::DotNuCompletion;
|
pub use dotnu_completions::DotNuCompletion;
|
||||||
|
pub use exportable_completions::ExportableCompletion;
|
||||||
pub use file_completions::{file_path_completion, FileCompletion};
|
pub use file_completions::{file_path_completion, FileCompletion};
|
||||||
pub use flag_completions::FlagCompletion;
|
pub use flag_completions::FlagCompletion;
|
||||||
pub use operator_completions::OperatorCompletion;
|
pub use operator_completions::OperatorCompletion;
|
||||||
|
@ -864,7 +864,7 @@ fn do_auto_cd(
|
|||||||
path.to_string_lossy().to_string()
|
path.to_string_lossy().to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let PermissionResult::PermissionDenied(_) = have_permission(path.clone()) {
|
if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
|
||||||
report_shell_error(
|
report_shell_error(
|
||||||
engine_state,
|
engine_state,
|
||||||
&ShellError::Io(IoError::new_with_additional_context(
|
&ShellError::Io(IoError::new_with_additional_context(
|
||||||
|
@ -10,7 +10,8 @@ use nu_cli::NuCompleter;
|
|||||||
use nu_engine::eval_block;
|
use nu_engine::eval_block;
|
||||||
use nu_parser::parse;
|
use nu_parser::parse;
|
||||||
use nu_path::expand_tilde;
|
use nu_path::expand_tilde;
|
||||||
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData};
|
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, Config, PipelineData};
|
||||||
|
use nu_std::load_standard_library;
|
||||||
use reedline::{Completer, Suggestion};
|
use reedline::{Completer, Suggestion};
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
use support::{
|
use support::{
|
||||||
@ -227,14 +228,12 @@ fn customcompletions_override_options() {
|
|||||||
let mut completer = custom_completer_with_options(
|
let mut completer = custom_completer_with_options(
|
||||||
r#"$env.config.completions.algorithm = "fuzzy"
|
r#"$env.config.completions.algorithm = "fuzzy"
|
||||||
$env.config.completions.case_sensitive = false"#,
|
$env.config.completions.case_sensitive = false"#,
|
||||||
r#"completion_algorithm: "prefix",
|
r#"completion_algorithm: "substring",
|
||||||
positional: false,
|
|
||||||
case_sensitive: true,
|
case_sensitive: true,
|
||||||
sort: true"#,
|
sort: true"#,
|
||||||
&["Foo Abcdef", "Abcdef", "Acd Bar"],
|
&["Foo Abcdef", "Abcdef", "Acd Bar"],
|
||||||
);
|
);
|
||||||
|
|
||||||
// positional: false should make it do substring matching
|
|
||||||
// sort: true should force sorting
|
// sort: true should force sorting
|
||||||
let expected: Vec<_> = vec!["Abcdef", "Foo Abcdef"];
|
let expected: Vec<_> = vec!["Abcdef", "Foo Abcdef"];
|
||||||
let suggestions = completer.complete("my-command Abcd", 15);
|
let suggestions = completer.complete("my-command Abcd", 15);
|
||||||
@ -350,9 +349,24 @@ fn custom_arguments_vs_subcommands() {
|
|||||||
match_suggestions(&expected, &suggestions);
|
match_suggestions(&expected, &suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_completions_defined_inline() {
|
||||||
|
let (_, _, engine, stack) = new_engine();
|
||||||
|
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
let completion_str = "def animals [] { [cat dog] }
|
||||||
|
export def say [
|
||||||
|
animal: string@animals
|
||||||
|
] { }; say ";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
// including only subcommand completions
|
||||||
|
let expected: Vec<_> = vec!["cat", "dog"];
|
||||||
|
match_suggestions(&expected, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
/// External command only if starts with `^`
|
/// External command only if starts with `^`
|
||||||
#[test]
|
#[test]
|
||||||
fn external_commands_only() {
|
fn external_commands() {
|
||||||
let engine = new_external_engine();
|
let engine = new_external_engine();
|
||||||
let mut completer = NuCompleter::new(
|
let mut completer = NuCompleter::new(
|
||||||
Arc::new(engine),
|
Arc::new(engine),
|
||||||
@ -375,6 +389,31 @@ fn external_commands_only() {
|
|||||||
match_suggestions(&expected, &suggestions);
|
match_suggestions(&expected, &suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disable external commands except for those start with `^`
|
||||||
|
#[test]
|
||||||
|
fn external_commands_disabled() {
|
||||||
|
let mut engine = new_external_engine();
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.completions.external.enable = false;
|
||||||
|
engine.set_config(config);
|
||||||
|
|
||||||
|
let stack = nu_protocol::engine::Stack::new();
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
let completion_str = "ls; ^sleep";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
#[cfg(windows)]
|
||||||
|
let expected: Vec<_> = vec!["sleep.exe"];
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let expected: Vec<_> = vec!["sleep"];
|
||||||
|
match_suggestions(&expected, &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "sleep";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
let expected: Vec<_> = vec!["sleep"];
|
||||||
|
match_suggestions(&expected, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
/// Which completes both internals and externals
|
/// Which completes both internals and externals
|
||||||
#[test]
|
#[test]
|
||||||
fn which_command_completions() {
|
fn which_command_completions() {
|
||||||
@ -473,7 +512,7 @@ fn dotnu_completions() {
|
|||||||
|
|
||||||
match_suggestions(&vec!["sub.nu`"], &suggestions);
|
match_suggestions(&vec!["sub.nu`"], &suggestions);
|
||||||
|
|
||||||
let expected = vec![
|
let mut expected = vec![
|
||||||
"asdf.nu",
|
"asdf.nu",
|
||||||
"bar.nu",
|
"bar.nu",
|
||||||
"bat.nu",
|
"bat.nu",
|
||||||
@ -506,6 +545,8 @@ fn dotnu_completions() {
|
|||||||
match_suggestions(&expected, &suggestions);
|
match_suggestions(&expected, &suggestions);
|
||||||
|
|
||||||
// Test use completion
|
// Test use completion
|
||||||
|
expected.push("std");
|
||||||
|
expected.push("std-rfc");
|
||||||
let completion_str = "use ";
|
let completion_str = "use ";
|
||||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
|
||||||
@ -537,6 +578,66 @@ fn dotnu_completions() {
|
|||||||
match_dir_content_for_dotnu(dir_content, &suggestions);
|
match_dir_content_for_dotnu(dir_content, &suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dotnu_stdlib_completions() {
|
||||||
|
let (_, _, mut engine, stack) = new_dotnu_engine();
|
||||||
|
assert!(load_standard_library(&mut engine).is_ok());
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
// `export use` should be recognized as command `export use`
|
||||||
|
let completion_str = "export use std/ass";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["assert"], &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "use `std-rfc/cli";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["clip"], &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "use \"std";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["\"std", "\"std-rfc"], &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "overlay use \'std-rfc/cli";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["clip"], &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exportable_completions() {
|
||||||
|
let (_, _, mut engine, mut stack) = new_dotnu_engine();
|
||||||
|
let code = r#"export module "🤔🐘" {
|
||||||
|
export const foo = "🤔🐘";
|
||||||
|
}"#;
|
||||||
|
assert!(support::merge_input(code.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
assert!(load_standard_library(&mut engine).is_ok());
|
||||||
|
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
let completion_str = "use std null";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["null-device", "null_device"], &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "export use std/assert eq";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["equal"], &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "use std/assert \"not eq";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["'not equal'"], &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "use std-rfc/clip ['prefi";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["prefix"], &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "use std/math [E, `TAU";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["TAU"], &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "use 🤔🐘 'foo";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["foo"], &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dotnu_completions_const_nu_lib_dirs() {
|
fn dotnu_completions_const_nu_lib_dirs() {
|
||||||
let (_, _, engine, stack) = new_dotnu_engine();
|
let (_, _, engine, stack) = new_dotnu_engine();
|
||||||
@ -911,10 +1012,11 @@ fn partial_completions() {
|
|||||||
// Create the expected values
|
// Create the expected values
|
||||||
let expected_paths = [
|
let expected_paths = [
|
||||||
file(dir.join("partial").join("hello.txt")),
|
file(dir.join("partial").join("hello.txt")),
|
||||||
|
folder(dir.join("partial").join("hol")),
|
||||||
file(dir.join("partial-a").join("have_ext.exe")),
|
file(dir.join("partial-a").join("have_ext.exe")),
|
||||||
file(dir.join("partial-a").join("have_ext.txt")),
|
file(dir.join("partial-a").join("have_ext.txt")),
|
||||||
file(dir.join("partial-a").join("hello")),
|
file(dir.join("partial-a").join("hello")),
|
||||||
file(dir.join("partial-a").join("hola")),
|
folder(dir.join("partial-a").join("hola")),
|
||||||
file(dir.join("partial-b").join("hello_b")),
|
file(dir.join("partial-b").join("hello_b")),
|
||||||
file(dir.join("partial-b").join("hi_b")),
|
file(dir.join("partial-b").join("hi_b")),
|
||||||
file(dir.join("partial-c").join("hello_c")),
|
file(dir.join("partial-c").join("hello_c")),
|
||||||
@ -931,11 +1033,12 @@ fn partial_completions() {
|
|||||||
// Create the expected values
|
// Create the expected values
|
||||||
let expected_paths = [
|
let expected_paths = [
|
||||||
file(dir.join("partial").join("hello.txt")),
|
file(dir.join("partial").join("hello.txt")),
|
||||||
|
folder(dir.join("partial").join("hol")),
|
||||||
file(dir.join("partial-a").join("anotherfile")),
|
file(dir.join("partial-a").join("anotherfile")),
|
||||||
file(dir.join("partial-a").join("have_ext.exe")),
|
file(dir.join("partial-a").join("have_ext.exe")),
|
||||||
file(dir.join("partial-a").join("have_ext.txt")),
|
file(dir.join("partial-a").join("have_ext.txt")),
|
||||||
file(dir.join("partial-a").join("hello")),
|
file(dir.join("partial-a").join("hello")),
|
||||||
file(dir.join("partial-a").join("hola")),
|
folder(dir.join("partial-a").join("hola")),
|
||||||
file(dir.join("partial-b").join("hello_b")),
|
file(dir.join("partial-b").join("hello_b")),
|
||||||
file(dir.join("partial-b").join("hi_b")),
|
file(dir.join("partial-b").join("hi_b")),
|
||||||
file(dir.join("partial-c").join("hello_c")),
|
file(dir.join("partial-c").join("hello_c")),
|
||||||
@ -1902,6 +2005,35 @@ fn table_cell_path_completions() {
|
|||||||
match_suggestions(&expected, &suggestions);
|
match_suggestions(&expected, &suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quoted_cell_path_completions() {
|
||||||
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
|
let command = r#"let foo = {'foo bar':1 'foo\\"bar"': 1 '.': 1 '|': 1 1: 1 "": 1}"#;
|
||||||
|
assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok());
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
let expected: Vec<_> = vec![
|
||||||
|
"\"\"",
|
||||||
|
"\".\"",
|
||||||
|
"\"1\"",
|
||||||
|
"\"foo bar\"",
|
||||||
|
"\"foo\\\\\\\\\\\"bar\\\"\"",
|
||||||
|
"\"|\"",
|
||||||
|
];
|
||||||
|
let completion_str = "$foo.";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&expected, &suggestions);
|
||||||
|
|
||||||
|
let expected: Vec<_> = vec!["\"foo bar\"", "\"foo\\\\\\\\\\\"bar\\\"\""];
|
||||||
|
let completion_str = "$foo.`foo";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&expected, &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "$foo.foo";
|
||||||
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
match_suggestions(&expected, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alias_of_command_and_flags() {
|
fn alias_of_command_and_flags() {
|
||||||
let (_, _, mut engine, mut stack) = new_engine();
|
let (_, _, mut engine, mut stack) = new_engine();
|
||||||
@ -2175,15 +2307,43 @@ fn exact_match() {
|
|||||||
|
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
// Troll case to test if exact match logic works case insensitively
|
||||||
let target_dir = format!("open {}", folder(dir.join("pArTiAL")));
|
let target_dir = format!("open {}", folder(dir.join("pArTiAL")));
|
||||||
let suggestions = completer.complete(&target_dir, target_dir.len());
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
// Since it's an exact match, only 'partial' should be suggested, not
|
|
||||||
// 'partial-a' and stuff. Implemented in #13302
|
|
||||||
match_suggestions(
|
match_suggestions(
|
||||||
&vec![file(dir.join("partial").join("hello.txt")).as_str()],
|
&vec![
|
||||||
|
file(dir.join("partial").join("hello.txt")).as_str(),
|
||||||
|
folder(dir.join("partial").join("hol")).as_str(),
|
||||||
|
],
|
||||||
&suggestions,
|
&suggestions,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let target_dir = format!("open {}", file(dir.join("partial").join("h")));
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
match_suggestions(
|
||||||
|
&vec![
|
||||||
|
file(dir.join("partial").join("hello.txt")).as_str(),
|
||||||
|
folder(dir.join("partial").join("hol")).as_str(),
|
||||||
|
],
|
||||||
|
&suggestions,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Even though "hol" is an exact match, the first component ("part") wasn't an
|
||||||
|
// exact match, so we include partial-a/hola
|
||||||
|
let target_dir = format!("open {}", file(dir.join("part").join("hol")));
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
match_suggestions(
|
||||||
|
&vec![
|
||||||
|
folder(dir.join("partial").join("hol")).as_str(),
|
||||||
|
folder(dir.join("partial-a").join("hola")).as_str(),
|
||||||
|
],
|
||||||
|
&suggestions,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exact match behavior shouldn't be enabled if the path has no slashes
|
||||||
|
let target_dir = format!("open {}", file(dir.join("partial")));
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
assert!(suggestions.len() > 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ignore = "was reverted, still needs fixing"]
|
#[ignore = "was reverted, still needs fixing"]
|
||||||
|
@ -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.103.0"
|
version = "0.104.1"
|
||||||
|
|
||||||
# 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.103.0"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||||
nu-path = { path = "../nu-path", version = "0.103.0" }
|
nu-path = { path = "../nu-path", version = "0.104.1" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||||
|
|
||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
miette = { workspace = true }
|
miette = { workspace = true }
|
||||||
|
@ -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.103.0"
|
version = "0.104.1"
|
||||||
|
|
||||||
# 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.103.0" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||||
nu-json = { version = "0.103.0", path = "../nu-json" }
|
nu-json = { version = "0.104.1", path = "../nu-json" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||||
nu-pretty-hex = { version = "0.103.0", path = "../nu-pretty-hex" }
|
nu-pretty-hex = { version = "0.104.1", path = "../nu-pretty-hex" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
|
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false }
|
||||||
|
|
||||||
# Potential dependencies for extras
|
# Potential dependencies for extras
|
||||||
heck = { workspace = true }
|
heck = { workspace = true }
|
||||||
@ -37,6 +37,6 @@ itertools = { workspace = true }
|
|||||||
mime = { workspace = true }
|
mime = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" }
|
||||||
nu-command = { path = "../nu-command", version = "0.103.0" }
|
nu-command = { path = "../nu-command", version = "0.104.1" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
|
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
|
||||||
|
@ -135,7 +135,7 @@ where
|
|||||||
(min, max) => (rhs, lhs, max, min),
|
(min, max) => (rhs, lhs, max, min),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pad = iter::repeat(0).take(max_len - min_len);
|
let pad = iter::repeat_n(0, max_len - min_len);
|
||||||
|
|
||||||
let mut a;
|
let mut a;
|
||||||
let mut b;
|
let mut b;
|
||||||
@ -159,9 +159,10 @@ where
|
|||||||
}
|
}
|
||||||
(Value::Binary { .. }, Value::Int { .. }) | (Value::Int { .. }, Value::Binary { .. }) => {
|
(Value::Binary { .. }, Value::Int { .. }) | (Value::Int { .. }, Value::Binary { .. }) => {
|
||||||
Value::error(
|
Value::error(
|
||||||
ShellError::PipelineMismatch {
|
ShellError::OnlySupportsThisInputType {
|
||||||
exp_input_type: "input, and argument, to be both int or both binary"
|
exp_input_type: "input, and argument, to be both int or both binary"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
wrong_type: "int and binary".to_string(),
|
||||||
dst_span: rhs.span(),
|
dst_span: rhs.span(),
|
||||||
src_span: span,
|
src_span: span,
|
||||||
},
|
},
|
||||||
|
@ -249,7 +249,7 @@ fn shift_bytes_and_bits_left(data: &[u8], byte_shift: usize, bit_shift: usize) -
|
|||||||
Last | Only => lhs << bit_shift,
|
Last | Only => lhs << bit_shift,
|
||||||
_ => (lhs << bit_shift) | (rhs >> (8 - bit_shift)),
|
_ => (lhs << bit_shift) | (rhs >> (8 - bit_shift)),
|
||||||
})
|
})
|
||||||
.chain(iter::repeat(0).take(byte_shift))
|
.chain(iter::repeat_n(0, byte_shift))
|
||||||
.collect::<Vec<u8>>()
|
.collect::<Vec<u8>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.103.0"
|
version = "0.104.1"
|
||||||
|
|
||||||
[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.103.0", default-features = false }
|
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
|
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false }
|
||||||
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
shadow-rs = { version = "0.38", default-features = false }
|
shadow-rs = { version = "1.1", default-features = false }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
shadow-rs = { version = "0.38", default-features = false }
|
shadow-rs = { version = "1.1", default-features = false, features = ["build"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
quickcheck = { workspace = true }
|
quickcheck = { workspace = true }
|
||||||
|
@ -18,4 +18,4 @@ A base crate is one with minimal dependencies in our system so that other develo
|
|||||||
|
|
||||||
### Background on nu-cmd-lang
|
### Background on nu-cmd-lang
|
||||||
|
|
||||||
This crate was designed to be a small, concise set of tools or commands that serve as the *foundation layer* of both nu and nushell. These are the core commands needed to have a nice working version of the *nu language* without all of the support that the other commands provide inside nushell. Prior to the launch of this crate all of our commands were housed in the crate *nu-command*. Moving forward we would like to *slowly* break out the commands in nu-command into different crates; the naming and how this will work and where all the commands will be located is a "work in progress" especially now that the *standard library* is starting to become more popular as a location for commands. As time goes on some of our commands written in rust will be migrated to nu and when this happens they will be moved into the *standard library*.
|
This crate was designed to be a small, concise set of tools or commands that serve as the *foundation layer* of both nu and nushell. These are the core commands needed to have a nice working version of the *nu language* without all of the support that the other commands provide inside nushell. Prior to the launch of this crate all of our commands were housed in the crate *nu-command*. Moving forward we would like to *slowly* break out the commands in nu-command into different crates; the naming and how this will work and where all the commands will be located is a "work in progress" especially now that the *standard library* is starting to become more popular as a location for commands. As time goes on some of our commands written in rust will be migrated to nu and when this happens they will be moved into the *standard library*.
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::{engine::StateWorkingSet, ByteStreamSource, PipelineMetadata};
|
use nu_protocol::{
|
||||||
|
engine::{Closure, StateWorkingSet},
|
||||||
|
BlockId, ByteStreamSource, Category, PipelineMetadata, Signature,
|
||||||
|
};
|
||||||
|
use std::any::type_name;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Describe;
|
pub struct Describe;
|
||||||
|
|
||||||
@ -73,39 +76,116 @@ impl Command for Describe {
|
|||||||
"{shell:'true', uwu:true, features: {bugs:false, multiplatform:true, speed: 10}, fib: [1 1 2 3 5 8], on_save: {|x| $'Saving ($x)'}, first_commit: 2019-05-10, my_duration: (4min + 20sec)} | describe -d",
|
"{shell:'true', uwu:true, features: {bugs:false, multiplatform:true, speed: 10}, fib: [1 1 2 3 5 8], on_save: {|x| $'Saving ($x)'}, first_commit: 2019-05-10, my_duration: (4min + 20sec)} | describe -d",
|
||||||
result: Some(Value::test_record(record!(
|
result: Some(Value::test_record(record!(
|
||||||
"type" => Value::test_string("record"),
|
"type" => Value::test_string("record"),
|
||||||
|
"detailed_type" => Value::test_string("record<shell: string, uwu: bool, features: record<bugs: bool, multiplatform: bool, speed: int>, fib: list<int>, on_save: closure, first_commit: datetime, my_duration: duration>"),
|
||||||
"columns" => Value::test_record(record!(
|
"columns" => Value::test_record(record!(
|
||||||
"shell" => Value::test_string("string"),
|
"shell" => Value::test_record(record!(
|
||||||
"uwu" => Value::test_string("bool"),
|
"type" => Value::test_string("string"),
|
||||||
|
"detailed_type" => Value::test_string("string"),
|
||||||
|
"rust_type" => Value::test_string("&alloc::string::String"),
|
||||||
|
"value" => Value::test_string("true"),
|
||||||
|
)),
|
||||||
|
"uwu" => Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("bool"),
|
||||||
|
"detailed_type" => Value::test_string("bool"),
|
||||||
|
"rust_type" => Value::test_string("bool"),
|
||||||
|
"value" => Value::test_bool(true),
|
||||||
|
)),
|
||||||
"features" => Value::test_record(record!(
|
"features" => Value::test_record(record!(
|
||||||
"type" => Value::test_string("record"),
|
"type" => Value::test_string("record"),
|
||||||
|
"detailed_type" => Value::test_string("record<bugs: bool, multiplatform: bool, speed: int>"),
|
||||||
"columns" => Value::test_record(record!(
|
"columns" => Value::test_record(record!(
|
||||||
"bugs" => Value::test_string("bool"),
|
"bugs" => Value::test_record(record!(
|
||||||
"multiplatform" => Value::test_string("bool"),
|
"type" => Value::test_string("bool"),
|
||||||
"speed" => Value::test_string("int"),
|
"detailed_type" => Value::test_string("bool"),
|
||||||
|
"rust_type" => Value::test_string("bool"),
|
||||||
|
"value" => Value::test_bool(false),
|
||||||
|
)),
|
||||||
|
"multiplatform" => Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("bool"),
|
||||||
|
"detailed_type" => Value::test_string("bool"),
|
||||||
|
"rust_type" => Value::test_string("bool"),
|
||||||
|
"value" => Value::test_bool(true),
|
||||||
|
)),
|
||||||
|
"speed" => Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("int"),
|
||||||
|
"detailed_type" => Value::test_string("int"),
|
||||||
|
"rust_type" => Value::test_string("i64"),
|
||||||
|
"value" => Value::test_int(10),
|
||||||
|
)),
|
||||||
)),
|
)),
|
||||||
|
"rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow<nu_protocol::value::record::Record>"),
|
||||||
)),
|
)),
|
||||||
"fib" => Value::test_record(record!(
|
"fib" => Value::test_record(record!(
|
||||||
"type" => Value::test_string("list"),
|
"type" => Value::test_string("list"),
|
||||||
|
"detailed_type" => Value::test_string("list<int>"),
|
||||||
"length" => Value::test_int(6),
|
"length" => Value::test_int(6),
|
||||||
"values" => Value::test_list(vec![
|
"rust_type" => Value::test_string("&mut alloc::vec::Vec<nu_protocol::value::Value>"),
|
||||||
Value::test_string("int"),
|
"value" => Value::test_list(vec![
|
||||||
Value::test_string("int"),
|
Value::test_record(record!(
|
||||||
Value::test_string("int"),
|
"type" => Value::test_string("int"),
|
||||||
Value::test_string("int"),
|
"detailed_type" => Value::test_string("int"),
|
||||||
Value::test_string("int"),
|
"rust_type" => Value::test_string("i64"),
|
||||||
Value::test_string("int"),
|
"value" => Value::test_int(1),
|
||||||
]),
|
)),
|
||||||
|
Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("int"),
|
||||||
|
"detailed_type" => Value::test_string("int"),
|
||||||
|
"rust_type" => Value::test_string("i64"),
|
||||||
|
"value" => Value::test_int(1),
|
||||||
|
)),
|
||||||
|
Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("int"),
|
||||||
|
"detailed_type" => Value::test_string("int"),
|
||||||
|
"rust_type" => Value::test_string("i64"),
|
||||||
|
"value" => Value::test_int(2),
|
||||||
|
)),
|
||||||
|
Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("int"),
|
||||||
|
"detailed_type" => Value::test_string("int"),
|
||||||
|
"rust_type" => Value::test_string("i64"),
|
||||||
|
"value" => Value::test_int(3),
|
||||||
|
)),
|
||||||
|
Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("int"),
|
||||||
|
"detailed_type" => Value::test_string("int"),
|
||||||
|
"rust_type" => Value::test_string("i64"),
|
||||||
|
"value" => Value::test_int(5),
|
||||||
|
)),
|
||||||
|
Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("int"),
|
||||||
|
"detailed_type" => Value::test_string("int"),
|
||||||
|
"rust_type" => Value::test_string("i64"),
|
||||||
|
"value" => Value::test_int(8),
|
||||||
|
))]
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
"on_save" => Value::test_record(record!(
|
"on_save" => Value::test_record(record!(
|
||||||
"type" => Value::test_string("closure"),
|
"type" => Value::test_string("closure"),
|
||||||
|
"detailed_type" => Value::test_string("closure"),
|
||||||
|
"rust_type" => Value::test_string("&alloc::boxed::Box<nu_protocol::engine::closure::Closure>"),
|
||||||
|
"value" => Value::test_closure(Closure {
|
||||||
|
block_id: BlockId::new(1),
|
||||||
|
captures: vec![],
|
||||||
|
}),
|
||||||
"signature" => Value::test_record(record!(
|
"signature" => Value::test_record(record!(
|
||||||
"name" => Value::test_string(""),
|
"name" => Value::test_string(""),
|
||||||
"category" => Value::test_string("default"),
|
"category" => Value::test_string("default"),
|
||||||
)),
|
)),
|
||||||
)),
|
)),
|
||||||
"first_commit" => Value::test_string("date"),
|
"first_commit" => Value::test_record(record!(
|
||||||
"my_duration" => Value::test_string("duration"),
|
"type" => Value::test_string("datetime"),
|
||||||
|
"detailed_type" => Value::test_string("datetime"),
|
||||||
|
"rust_type" => Value::test_string("chrono::datetime::DateTime<chrono::offset::fixed::FixedOffset>"),
|
||||||
|
"value" => Value::test_date("2019-05-10 00:00:00Z".parse().unwrap_or_default()),
|
||||||
|
)),
|
||||||
|
"my_duration" => Value::test_record(record!(
|
||||||
|
"type" => Value::test_string("duration"),
|
||||||
|
"detailed_type" => Value::test_string("duration"),
|
||||||
|
"rust_type" => Value::test_string("i64"),
|
||||||
|
"value" => Value::test_duration(260_000_000_000),
|
||||||
|
))
|
||||||
)),
|
)),
|
||||||
|
"rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow<nu_protocol::value::record::Record>"),
|
||||||
))),
|
))),
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
@ -175,7 +255,9 @@ fn run(
|
|||||||
|
|
||||||
Value::record(
|
Value::record(
|
||||||
record! {
|
record! {
|
||||||
"type" => Value::string(type_, head),
|
"type" => Value::string("bytestream", head),
|
||||||
|
"detailed_type" => Value::string(type_, head),
|
||||||
|
"rust_type" => Value::string(type_of(&stream), head),
|
||||||
"origin" => Value::string(origin, head),
|
"origin" => Value::string(origin, head),
|
||||||
"metadata" => metadata_to_value(metadata, head),
|
"metadata" => metadata_to_value(metadata, head),
|
||||||
},
|
},
|
||||||
@ -192,6 +274,7 @@ fn run(
|
|||||||
description
|
description
|
||||||
}
|
}
|
||||||
PipelineData::ListStream(stream, ..) => {
|
PipelineData::ListStream(stream, ..) => {
|
||||||
|
let type_ = type_of(&stream);
|
||||||
if options.detailed {
|
if options.detailed {
|
||||||
let subtype = if options.no_collect {
|
let subtype = if options.no_collect {
|
||||||
Value::string("any", head)
|
Value::string("any", head)
|
||||||
@ -201,6 +284,8 @@ fn run(
|
|||||||
Value::record(
|
Value::record(
|
||||||
record! {
|
record! {
|
||||||
"type" => Value::string("stream", head),
|
"type" => Value::string("stream", head),
|
||||||
|
"detailed_type" => Value::string("list stream", head),
|
||||||
|
"rust_type" => Value::string(type_, head),
|
||||||
"origin" => Value::string("nushell", head),
|
"origin" => Value::string("nushell", head),
|
||||||
"subtype" => subtype,
|
"subtype" => subtype,
|
||||||
"metadata" => metadata_to_value(metadata, head),
|
"metadata" => metadata_to_value(metadata, head),
|
||||||
@ -229,45 +314,95 @@ fn run(
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Description {
|
enum Description {
|
||||||
String(String),
|
|
||||||
Record(Record),
|
Record(Record),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Description {
|
impl Description {
|
||||||
fn into_value(self, span: Span) -> Value {
|
fn into_value(self, span: Span) -> Value {
|
||||||
match self {
|
match self {
|
||||||
Description::String(ty) => Value::string(ty, span),
|
|
||||||
Description::Record(record) => Value::record(record, span),
|
Description::Record(record) => Value::record(record, span),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn describe_value(value: Value, head: Span, engine_state: Option<&EngineState>) -> Value {
|
fn describe_value(value: Value, head: Span, engine_state: Option<&EngineState>) -> Value {
|
||||||
let record = match describe_value_inner(value, head, engine_state) {
|
let Description::Record(record) = describe_value_inner(value, head, engine_state);
|
||||||
Description::String(ty) => record! { "type" => Value::string(ty, head) },
|
|
||||||
Description::Record(record) => record,
|
|
||||||
};
|
|
||||||
Value::record(record, head)
|
Value::record(record, head)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn type_of<T>(_: &T) -> String {
|
||||||
|
type_name::<T>().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn describe_value_inner(
|
fn describe_value_inner(
|
||||||
value: Value,
|
mut value: Value,
|
||||||
head: Span,
|
head: Span,
|
||||||
engine_state: Option<&EngineState>,
|
engine_state: Option<&EngineState>,
|
||||||
) -> Description {
|
) -> Description {
|
||||||
|
let value_type = value.get_type().to_string();
|
||||||
match value {
|
match value {
|
||||||
Value::Bool { .. }
|
Value::Bool { val, .. } => Description::Record(record! {
|
||||||
| Value::Int { .. }
|
"type" => Value::string("bool", head),
|
||||||
| Value::Float { .. }
|
"detailed_type" => Value::string(value_type, head),
|
||||||
| Value::Filesize { .. }
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
| Value::Duration { .. }
|
"value" => value,
|
||||||
| Value::Date { .. }
|
}),
|
||||||
| Value::Range { .. }
|
Value::Int { val, .. } => Description::Record(record! {
|
||||||
| Value::String { .. }
|
"type" => Value::string("int", head),
|
||||||
| Value::Glob { .. }
|
"detailed_type" => Value::string(value_type, head),
|
||||||
| Value::Nothing { .. } => Description::String(value.get_type().to_string()),
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
Value::Record { val, .. } => {
|
"value" => value,
|
||||||
let mut columns = val.into_owned();
|
}),
|
||||||
|
Value::Float { val, .. } => Description::Record(record! {
|
||||||
|
"type" => Value::string("float", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
|
}),
|
||||||
|
Value::Filesize { val, .. } => Description::Record(record! {
|
||||||
|
"type" => Value::string("filesize", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
|
}),
|
||||||
|
Value::Duration { val, .. } => Description::Record(record! {
|
||||||
|
"type" => Value::string("duration", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
|
}),
|
||||||
|
Value::Date { val, .. } => Description::Record(record! {
|
||||||
|
"type" => Value::string("datetime", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
|
}),
|
||||||
|
Value::Range { ref val, .. } => Description::Record(record! {
|
||||||
|
"type" => Value::string("range", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
|
}),
|
||||||
|
Value::String { ref val, .. } => Description::Record(record! {
|
||||||
|
"type" => Value::string("string", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
|
}),
|
||||||
|
Value::Glob { ref val, .. } => Description::Record(record! {
|
||||||
|
"type" => Value::string("glob", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
|
}),
|
||||||
|
Value::Nothing { .. } => Description::Record(record! {
|
||||||
|
"type" => Value::string("nothing", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string("", head),
|
||||||
|
"value" => value,
|
||||||
|
}),
|
||||||
|
Value::Record { ref val, .. } => {
|
||||||
|
let mut columns = val.clone().into_owned();
|
||||||
for (_, val) in &mut columns {
|
for (_, val) in &mut columns {
|
||||||
*val =
|
*val =
|
||||||
describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
|
describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
|
||||||
@ -275,25 +410,34 @@ fn describe_value_inner(
|
|||||||
|
|
||||||
Description::Record(record! {
|
Description::Record(record! {
|
||||||
"type" => Value::string("record", head),
|
"type" => Value::string("record", head),
|
||||||
"columns" => Value::record(columns, head),
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"columns" => Value::record(columns.clone(), head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Value::List { mut vals, .. } => {
|
Value::List { ref mut vals, .. } => {
|
||||||
for val in &mut vals {
|
for val in &mut *vals {
|
||||||
*val =
|
*val =
|
||||||
describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
|
describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
|
||||||
}
|
}
|
||||||
|
|
||||||
Description::Record(record! {
|
Description::Record(record! {
|
||||||
"type" => Value::string("list", head),
|
"type" => Value::string("list", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
"length" => Value::int(vals.len() as i64, head),
|
"length" => Value::int(vals.len() as i64, head),
|
||||||
"values" => Value::list(vals, head),
|
"rust_type" => Value::string(type_of(&vals), head),
|
||||||
|
"value" => value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Value::Closure { val, .. } => {
|
Value::Closure { ref val, .. } => {
|
||||||
let block = engine_state.map(|engine_state| engine_state.get_block(val.block_id));
|
let block = engine_state.map(|engine_state| engine_state.get_block(val.block_id));
|
||||||
|
|
||||||
let mut record = record! { "type" => Value::string("closure", head) };
|
let mut record = record! {
|
||||||
|
"type" => Value::string("closure", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
|
};
|
||||||
if let Some(block) = block {
|
if let Some(block) = block {
|
||||||
record.push(
|
record.push(
|
||||||
"signature",
|
"signature",
|
||||||
@ -308,21 +452,37 @@ fn describe_value_inner(
|
|||||||
}
|
}
|
||||||
Description::Record(record)
|
Description::Record(record)
|
||||||
}
|
}
|
||||||
Value::Error { error, .. } => Description::Record(record! {
|
Value::Error { ref error, .. } => Description::Record(record! {
|
||||||
"type" => Value::string("error", head),
|
"type" => Value::string("error", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
"subtype" => Value::string(error.to_string(), head),
|
"subtype" => Value::string(error.to_string(), head),
|
||||||
|
"rust_type" => Value::string(type_of(&error), head),
|
||||||
|
"value" => value,
|
||||||
}),
|
}),
|
||||||
Value::Binary { val, .. } => Description::Record(record! {
|
Value::Binary { ref val, .. } => Description::Record(record! {
|
||||||
"type" => Value::string("binary", head),
|
"type" => Value::string("binary", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
"length" => Value::int(val.len() as i64, head),
|
"length" => Value::int(val.len() as i64, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value,
|
||||||
}),
|
}),
|
||||||
Value::CellPath { val, .. } => Description::Record(record! {
|
Value::CellPath { ref val, .. } => Description::Record(record! {
|
||||||
"type" => Value::string("cell-path", head),
|
"type" => Value::string("cell-path", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
"length" => Value::int(val.members.len() as i64, head),
|
"length" => Value::int(val.members.len() as i64, head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" => value
|
||||||
}),
|
}),
|
||||||
Value::Custom { val, .. } => Description::Record(record! {
|
Value::Custom { ref val, .. } => Description::Record(record! {
|
||||||
"type" => Value::string("custom", head),
|
"type" => Value::string("custom", head),
|
||||||
|
"detailed_type" => Value::string(value_type, head),
|
||||||
"subtype" => Value::string(val.type_name(), head),
|
"subtype" => Value::string(val.type_name(), head),
|
||||||
|
"rust_type" => Value::string(type_of(&val), head),
|
||||||
|
"value" =>
|
||||||
|
match val.to_base_value(head) {
|
||||||
|
Ok(base_value) => base_value,
|
||||||
|
Err(err) => Value::error(err, head),
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,16 +31,6 @@ impl Command for Do {
|
|||||||
"ignore errors as the closure runs",
|
"ignore errors as the closure runs",
|
||||||
Some('i'),
|
Some('i'),
|
||||||
)
|
)
|
||||||
.switch(
|
|
||||||
"ignore-shell-errors",
|
|
||||||
"ignore shell errors as the closure runs",
|
|
||||||
Some('s'),
|
|
||||||
)
|
|
||||||
.switch(
|
|
||||||
"ignore-program-errors",
|
|
||||||
"ignore external program errors as the closure runs",
|
|
||||||
Some('p'),
|
|
||||||
)
|
|
||||||
.switch(
|
.switch(
|
||||||
"capture-errors",
|
"capture-errors",
|
||||||
"catch errors as the closure runs, and return them",
|
"catch errors as the closure runs, and return them",
|
||||||
@ -71,36 +61,6 @@ impl Command for Do {
|
|||||||
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
|
let rest: Vec<Value> = call.rest(engine_state, caller_stack, 1)?;
|
||||||
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?;
|
let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?;
|
||||||
|
|
||||||
if call.has_flag(engine_state, caller_stack, "ignore-shell-errors")? {
|
|
||||||
nu_protocol::report_shell_warning(
|
|
||||||
engine_state,
|
|
||||||
&ShellError::GenericError {
|
|
||||||
error: "Deprecated option".into(),
|
|
||||||
msg: "`--ignore-shell-errors` is deprecated and will be removed in 0.102.0."
|
|
||||||
.into(),
|
|
||||||
span: Some(call.head),
|
|
||||||
help: Some("Please use the `--ignore-errors(-i)`".into()),
|
|
||||||
inner: vec![],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if call.has_flag(engine_state, caller_stack, "ignore-program-errors")? {
|
|
||||||
nu_protocol::report_shell_warning(
|
|
||||||
engine_state,
|
|
||||||
&ShellError::GenericError {
|
|
||||||
error: "Deprecated option".into(),
|
|
||||||
msg: "`--ignore-program-errors` is deprecated and will be removed in 0.102.0."
|
|
||||||
.into(),
|
|
||||||
span: Some(call.head),
|
|
||||||
help: Some("Please use the `--ignore-errors(-i)`".into()),
|
|
||||||
inner: vec![],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let ignore_shell_errors = ignore_all_errors
|
|
||||||
|| call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?;
|
|
||||||
let ignore_program_errors = ignore_all_errors
|
|
||||||
|| call.has_flag(engine_state, caller_stack, "ignore-program-errors")?;
|
|
||||||
let capture_errors = call.has_flag(engine_state, caller_stack, "capture-errors")?;
|
let capture_errors = call.has_flag(engine_state, caller_stack, "capture-errors")?;
|
||||||
let has_env = call.has_flag(engine_state, caller_stack, "env")?;
|
let has_env = call.has_flag(engine_state, caller_stack, "env")?;
|
||||||
|
|
||||||
@ -206,7 +166,7 @@ impl Command for Do {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(PipelineData::ByteStream(mut stream, metadata))
|
Ok(PipelineData::ByteStream(mut stream, metadata))
|
||||||
if ignore_program_errors
|
if ignore_all_errors
|
||||||
&& !matches!(
|
&& !matches!(
|
||||||
caller_stack.stdout(),
|
caller_stack.stdout(),
|
||||||
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value
|
OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value
|
||||||
@ -218,10 +178,10 @@ impl Command for Do {
|
|||||||
}
|
}
|
||||||
Ok(PipelineData::ByteStream(stream, metadata))
|
Ok(PipelineData::ByteStream(stream, metadata))
|
||||||
}
|
}
|
||||||
Ok(PipelineData::Value(Value::Error { .. }, ..)) | Err(_) if ignore_shell_errors => {
|
Ok(PipelineData::Value(Value::Error { .. }, ..)) | Err(_) if ignore_all_errors => {
|
||||||
Ok(PipelineData::empty())
|
Ok(PipelineData::empty())
|
||||||
}
|
}
|
||||||
Ok(PipelineData::ListStream(stream, metadata)) if ignore_shell_errors => {
|
Ok(PipelineData::ListStream(stream, metadata)) if ignore_all_errors => {
|
||||||
let stream = stream.map(move |value| {
|
let stream = stream.map(move |value| {
|
||||||
if let Value::Error { .. } = value {
|
if let Value::Error { .. } = value {
|
||||||
Value::nothing(head)
|
Value::nothing(head)
|
||||||
|
@ -116,6 +116,8 @@ impl Command for OverlayUse {
|
|||||||
// b) refreshing an active overlay (the origin module changed)
|
// b) refreshing an active overlay (the origin module changed)
|
||||||
|
|
||||||
let module = engine_state.get_module(module_id);
|
let module = engine_state.get_module(module_id);
|
||||||
|
// in such case, should also make sure that PWD is not restored in old overlays.
|
||||||
|
let cwd = caller_stack.get_env_var(engine_state, "PWD").cloned();
|
||||||
|
|
||||||
// Evaluate the export-env block (if any) and keep its environment
|
// Evaluate the export-env block (if any) and keep its environment
|
||||||
if let Some(block_id) = module.env_block {
|
if let Some(block_id) = module.env_block {
|
||||||
@ -160,11 +162,19 @@ impl Command for OverlayUse {
|
|||||||
|
|
||||||
// The export-env block should see the env vars *before* activating this overlay
|
// The export-env block should see the env vars *before* activating this overlay
|
||||||
caller_stack.add_overlay(overlay_name);
|
caller_stack.add_overlay(overlay_name);
|
||||||
|
// make sure that PWD is not restored in old overlays.
|
||||||
|
if let Some(cwd) = cwd {
|
||||||
|
caller_stack.add_env_var("PWD".to_string(), cwd);
|
||||||
|
}
|
||||||
|
|
||||||
// Merge the block's environment to the current stack
|
// Merge the block's environment to the current stack
|
||||||
redirect_env(engine_state, caller_stack, &callee_stack);
|
redirect_env(engine_state, caller_stack, &callee_stack);
|
||||||
} else {
|
} else {
|
||||||
caller_stack.add_overlay(overlay_name);
|
caller_stack.add_overlay(overlay_name);
|
||||||
|
// make sure that PWD is not restored in old overlays.
|
||||||
|
if let Some(cwd) = cwd {
|
||||||
|
caller_stack.add_env_var("PWD".to_string(), cwd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
caller_stack.add_overlay(overlay_name);
|
caller_stack.add_overlay(overlay_name);
|
||||||
|
@ -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.103.0"
|
version = "0.104.1"
|
||||||
|
|
||||||
# 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.103.0"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-engine = { path = "../nu-engine", version = "0.103.0" }
|
nu-engine = { path = "../nu-engine", version = "0.104.1" }
|
||||||
nu-path = { path = "../nu-path", version = "0.103.0" }
|
nu-path = { path = "../nu-path", version = "0.104.1" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", features = ["plugin"] }
|
nu-protocol = { path = "../nu-protocol", version = "0.104.1", features = ["plugin"] }
|
||||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.103.0" }
|
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.104.1" }
|
||||||
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
|
|
||||||
|
@ -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.103.0"
|
version = "0.104.1"
|
||||||
|
|
||||||
[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.103.0", default-features = false }
|
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||||
nu-json = { path = "../nu-json", version = "0.103.0" }
|
nu-json = { path = "../nu-json", version = "0.104.1" }
|
||||||
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.103.0" }
|
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
|
@ -120,7 +120,7 @@ impl<'a> StyleComputer<'a> {
|
|||||||
("int".to_string(), ComputableStyle::Static(Color::White.normal())),
|
("int".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||||
("filesize".to_string(), ComputableStyle::Static(Color::Cyan.normal())),
|
("filesize".to_string(), ComputableStyle::Static(Color::Cyan.normal())),
|
||||||
("duration".to_string(), ComputableStyle::Static(Color::White.normal())),
|
("duration".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||||
("date".to_string(), ComputableStyle::Static(Color::Purple.normal())),
|
("datetime".to_string(), ComputableStyle::Static(Color::Purple.normal())),
|
||||||
("range".to_string(), ComputableStyle::Static(Color::White.normal())),
|
("range".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||||
("float".to_string(), ComputableStyle::Static(Color::White.normal())),
|
("float".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||||
("string".to_string(), ComputableStyle::Static(Color::White.normal())),
|
("string".to_string(), ComputableStyle::Static(Color::White.normal())),
|
||||||
|
@ -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.103.0"
|
version = "0.104.1"
|
||||||
|
|
||||||
# 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.103.0" }
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" }
|
||||||
nu-color-config = { path = "../nu-color-config", version = "0.103.0" }
|
nu-color-config = { path = "../nu-color-config", version = "0.104.1" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.103.0", default-features = false }
|
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false }
|
||||||
nu-glob = { path = "../nu-glob", version = "0.103.0" }
|
nu-glob = { path = "../nu-glob", version = "0.104.1" }
|
||||||
nu-json = { path = "../nu-json", version = "0.103.0" }
|
nu-json = { path = "../nu-json", version = "0.104.1" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.103.0" }
|
nu-parser = { path = "../nu-parser", version = "0.104.1" }
|
||||||
nu-path = { path = "../nu-path", version = "0.103.0" }
|
nu-path = { path = "../nu-path", version = "0.104.1" }
|
||||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.103.0" }
|
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.104.1" }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.103.0", default-features = false }
|
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false }
|
||||||
nu-system = { path = "../nu-system", version = "0.103.0" }
|
nu-system = { path = "../nu-system", version = "0.104.1" }
|
||||||
nu-table = { path = "../nu-table", version = "0.103.0" }
|
nu-table = { path = "../nu-table", version = "0.104.1" }
|
||||||
nu-term-grid = { path = "../nu-term-grid", version = "0.103.0" }
|
nu-term-grid = { path = "../nu-term-grid", version = "0.104.1" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.103.0", default-features = false }
|
nu-utils = { path = "../nu-utils", version = "0.104.1", default-features = false }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
nuon = { path = "../nuon", version = "0.103.0" }
|
nuon = { path = "../nuon", version = "0.104.1" }
|
||||||
|
|
||||||
alphanumeric-sort = { workspace = true }
|
alphanumeric-sort = { workspace = true }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
@ -129,7 +129,7 @@ v_htmlescape = { workspace = true }
|
|||||||
wax = { workspace = true }
|
wax = { workspace = true }
|
||||||
which = { workspace = true, optional = true }
|
which = { workspace = true, optional = true }
|
||||||
unicode-width = { workspace = true }
|
unicode-width = { workspace = true }
|
||||||
data-encoding = { version = "2.8.0", features = ["alloc"] }
|
data-encoding = { version = "2.9.0", features = ["alloc"] }
|
||||||
web-time = { workspace = true }
|
web-time = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
@ -209,8 +209,8 @@ sqlite = ["rusqlite"]
|
|||||||
trash-support = ["trash"]
|
trash-support = ["trash"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.0" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" }
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.103.0" }
|
nu-test-support = { path = "../nu-test-support", version = "0.104.1" }
|
||||||
|
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
mockito = { workspace = true, default-features = false }
|
mockito = { workspace = true, default-features = false }
|
||||||
|
@ -1,12 +1,29 @@
|
|||||||
use crate::{generate_strftime_list, parse_date_from_string};
|
use crate::{generate_strftime_list, parse_date_from_string};
|
||||||
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc};
|
use chrono::{
|
||||||
use human_date_parser::{from_human_time, ParseResult};
|
DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone,
|
||||||
|
Timelike, Utc,
|
||||||
|
};
|
||||||
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::*;
|
||||||
|
|
||||||
|
const HOUR: i32 = 60 * 60;
|
||||||
|
const ALLOWED_COLUMNS: [&str; 10] = [
|
||||||
|
"year",
|
||||||
|
"month",
|
||||||
|
"day",
|
||||||
|
"hour",
|
||||||
|
"minute",
|
||||||
|
"second",
|
||||||
|
"millisecond",
|
||||||
|
"microsecond",
|
||||||
|
"nanosecond",
|
||||||
|
"timezone",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
struct Arguments {
|
struct Arguments {
|
||||||
zone_options: Option<Spanned<Zone>>,
|
zone_options: Option<Spanned<Zone>>,
|
||||||
format_options: Option<DatetimeFormat>,
|
format_options: Option<Spanned<DatetimeFormat>>,
|
||||||
cell_paths: Option<Vec<CellPath>>,
|
cell_paths: Option<Vec<CellPath>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,8 +81,12 @@ impl Command for IntoDatetime {
|
|||||||
(Type::String, Type::Date),
|
(Type::String, Type::Date),
|
||||||
(Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Date))),
|
(Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Date))),
|
||||||
(Type::table(), Type::table()),
|
(Type::table(), Type::table()),
|
||||||
(Type::record(), Type::record()),
|
|
||||||
(Type::Nothing, Type::table()),
|
(Type::Nothing, Type::table()),
|
||||||
|
// FIXME: https://github.com/nushell/nushell/issues/15485
|
||||||
|
// 'record -> any' was added as a temporary workaround to avoid type inference issues. The Any arm needs to be appear first.
|
||||||
|
(Type::record(), Type::Any),
|
||||||
|
(Type::record(), Type::record()),
|
||||||
|
(Type::record(), Type::Date),
|
||||||
// FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
|
// FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
|
||||||
// which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
|
// which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
|
||||||
// only applicable for --list flag
|
// only applicable for --list flag
|
||||||
@ -95,11 +116,6 @@ impl Command for IntoDatetime {
|
|||||||
"Show all possible variables for use in --format flag",
|
"Show all possible variables for use in --format flag",
|
||||||
Some('l'),
|
Some('l'),
|
||||||
)
|
)
|
||||||
.switch(
|
|
||||||
"list-human",
|
|
||||||
"Show human-readable datetime parsing examples",
|
|
||||||
Some('n'),
|
|
||||||
)
|
|
||||||
.rest(
|
.rest(
|
||||||
"rest",
|
"rest",
|
||||||
SyntaxShape::CellPath,
|
SyntaxShape::CellPath,
|
||||||
@ -117,8 +133,6 @@ impl Command for IntoDatetime {
|
|||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
if call.has_flag(engine_state, stack, "list")? {
|
if call.has_flag(engine_state, stack, "list")? {
|
||||||
Ok(generate_strftime_list(call.head, true).into_pipeline_data())
|
Ok(generate_strftime_list(call.head, true).into_pipeline_data())
|
||||||
} else if call.has_flag(engine_state, stack, "list-human")? {
|
|
||||||
Ok(list_human_readable_examples(call.head).into_pipeline_data())
|
|
||||||
} else {
|
} else {
|
||||||
let cell_paths = call.rest(engine_state, stack, 0)?;
|
let cell_paths = call.rest(engine_state, stack, 0)?;
|
||||||
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||||
@ -138,13 +152,16 @@ impl Command for IntoDatetime {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let format_options = call
|
let format_options = call
|
||||||
.get_flag::<String>(engine_state, stack, "format")?
|
.get_flag::<Spanned<String>>(engine_state, stack, "format")?
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|fmt| DatetimeFormat(fmt.to_string()));
|
.map(|fmt| Spanned {
|
||||||
|
item: DatetimeFormat(fmt.item.to_string()),
|
||||||
|
span: fmt.span,
|
||||||
|
});
|
||||||
|
|
||||||
let args = Arguments {
|
let args = Arguments {
|
||||||
format_options,
|
|
||||||
zone_options,
|
zone_options,
|
||||||
|
format_options,
|
||||||
cell_paths,
|
cell_paths,
|
||||||
};
|
};
|
||||||
operate(action, args, input, call.head, engine_state.signals())
|
operate(action, args, input, call.head, engine_state.signals())
|
||||||
@ -220,6 +237,12 @@ impl Command for IntoDatetime {
|
|||||||
#[allow(clippy::inconsistent_digit_grouping)]
|
#[allow(clippy::inconsistent_digit_grouping)]
|
||||||
result: example_result_1(1614434140_000000000),
|
result: example_result_1(1614434140_000000000),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Using a record as input",
|
||||||
|
example: "{year: 2025, month: 3, day: 30, hour: 12, minute: 15, second: 59, timezone: '+02:00'} | into datetime",
|
||||||
|
#[allow(clippy::inconsistent_digit_grouping)]
|
||||||
|
result: example_result_1(1743329759_000000000),
|
||||||
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Convert list of timestamps to datetimes",
|
description: "Convert list of timestamps to datetimes",
|
||||||
example: r#"["2023-03-30 10:10:07 -05:00", "2023-05-05 13:43:49 -05:00", "2023-06-05 01:37:42 -05:00"] | into datetime"#,
|
example: r#"["2023-03-30 10:10:07 -05:00", "2023-05-05 13:43:49 -05:00", "2023-06-05 01:37:42 -05:00"] | into datetime"#,
|
||||||
@ -253,26 +276,11 @@ impl Command for IntoDatetime {
|
|||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Example {
|
|
||||||
description: "Parsing human readable datetimes",
|
|
||||||
example: "'Today at 18:30' | into datetime",
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
Example {
|
|
||||||
description: "Parsing human readable datetimes",
|
|
||||||
example: "'Last Friday at 19:45' | into datetime",
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
Example {
|
|
||||||
description: "Parsing human readable datetimes",
|
|
||||||
example: "'In 5 minutes and 30 seconds' | into datetime",
|
|
||||||
result: None,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
struct DatetimeFormat(String);
|
struct DatetimeFormat(String);
|
||||||
|
|
||||||
fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||||
@ -284,45 +292,44 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
return input.clone();
|
return input.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Value::Record { val: record, .. } = input {
|
||||||
|
if let Some(tz) = timezone {
|
||||||
|
return Value::error(
|
||||||
|
ShellError::IncompatibleParameters {
|
||||||
|
left_message: "got a record as input".into(),
|
||||||
|
left_span: head,
|
||||||
|
right_message: "the timezone should be included in the record".into(),
|
||||||
|
right_span: tz.span,
|
||||||
|
},
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dt) = dateformat {
|
||||||
|
return Value::error(
|
||||||
|
ShellError::IncompatibleParameters {
|
||||||
|
left_message: "got a record as input".into(),
|
||||||
|
left_span: head,
|
||||||
|
right_message: "cannot be used with records".into(),
|
||||||
|
right_span: dt.span,
|
||||||
|
},
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let span = input.span();
|
||||||
|
return merge_record(record, head, span).unwrap_or_else(|err| Value::error(err, span));
|
||||||
|
}
|
||||||
|
|
||||||
// Let's try dtparse first
|
// Let's try dtparse first
|
||||||
if matches!(input, Value::String { .. }) && dateformat.is_none() {
|
if matches!(input, Value::String { .. }) && dateformat.is_none() {
|
||||||
let span = input.span();
|
let span = input.span();
|
||||||
if let Ok(input_val) = input.coerce_str() {
|
if let Ok(input_val) = input.coerce_str() {
|
||||||
match parse_date_from_string(&input_val, span) {
|
if let Ok(date) = parse_date_from_string(&input_val, span) {
|
||||||
Ok(date) => return Value::date(date, span),
|
return Value::date(date, span);
|
||||||
Err(_) => {
|
}
|
||||||
if let Ok(date) = from_human_time(&input_val) {
|
|
||||||
match date {
|
|
||||||
ParseResult::Date(date) => {
|
|
||||||
let time = Local::now().time();
|
|
||||||
let combined = date.and_time(time);
|
|
||||||
let local_offset = *Local::now().offset();
|
|
||||||
let dt_fixed =
|
|
||||||
TimeZone::from_local_datetime(&local_offset, &combined)
|
|
||||||
.single()
|
|
||||||
.unwrap_or_default();
|
|
||||||
return Value::date(dt_fixed, span);
|
|
||||||
}
|
|
||||||
ParseResult::DateTime(date) => {
|
|
||||||
return Value::date(date.fixed_offset(), span)
|
|
||||||
}
|
|
||||||
ParseResult::Time(time) => {
|
|
||||||
let date = Local::now().date_naive();
|
|
||||||
let combined = date.and_time(time);
|
|
||||||
let local_offset = *Local::now().offset();
|
|
||||||
let dt_fixed =
|
|
||||||
TimeZone::from_local_datetime(&local_offset, &combined)
|
|
||||||
.single()
|
|
||||||
.unwrap_or_default();
|
|
||||||
return Value::date(dt_fixed, span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const HOUR: i32 = 60 * 60;
|
|
||||||
|
|
||||||
// Check to see if input looks like a Unix timestamp (i.e. can it be parsed to an int?)
|
// Check to see if input looks like a Unix timestamp (i.e. can it be parsed to an int?)
|
||||||
let timestamp = match input {
|
let timestamp = match input {
|
||||||
@ -403,23 +410,59 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
|
|
||||||
let parse_as_string = |val: &str| {
|
let parse_as_string = |val: &str| {
|
||||||
match dateformat {
|
match dateformat {
|
||||||
Some(dt) => match DateTime::parse_from_str(val, &dt.0) {
|
Some(dt_format) => match DateTime::parse_from_str(val, &dt_format.item.0) {
|
||||||
Ok(d) => Value::date ( d, head ),
|
Ok(dt) => {
|
||||||
Err(reason) => {
|
match timezone {
|
||||||
match NaiveDateTime::parse_from_str(val, &dt.0) {
|
None => {
|
||||||
Ok(d) => {
|
Value::date ( dt, head )
|
||||||
let dt_fixed =
|
},
|
||||||
Local.from_local_datetime(&d).single().unwrap_or_default();
|
Some(Spanned { item, span }) => match item {
|
||||||
|
Zone::Utc => {
|
||||||
Value::date(dt_fixed.into(),head)
|
Value::date ( dt, head )
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Zone::Local => {
|
||||||
Value::error (
|
Value::date(dt.with_timezone(&Local).into(), *span)
|
||||||
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()) },
|
}
|
||||||
head,
|
Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
|
||||||
)
|
Some(eastoffset) => {
|
||||||
}
|
Value::date(dt.with_timezone(&eastoffset), *span)
|
||||||
|
}
|
||||||
|
None => Value::error(
|
||||||
|
ShellError::DatetimeParseError {
|
||||||
|
msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
|
||||||
|
span: *span,
|
||||||
|
},
|
||||||
|
*span,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
|
||||||
|
Some(westoffset) => {
|
||||||
|
Value::date(dt.with_timezone(&westoffset), *span)
|
||||||
|
}
|
||||||
|
None => Value::error(
|
||||||
|
ShellError::DatetimeParseError {
|
||||||
|
msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
|
||||||
|
span: *span,
|
||||||
|
},
|
||||||
|
*span,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Zone::Error => Value::error(
|
||||||
|
// This is an argument error, not an input error
|
||||||
|
ShellError::TypeMismatch {
|
||||||
|
err_message: "Invalid timezone or offset".to_string(),
|
||||||
|
span: *span,
|
||||||
|
},
|
||||||
|
*span,
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(reason) => {
|
||||||
|
parse_with_format(val, &dt_format.item.0, head).unwrap_or_else(|_| Value::error (
|
||||||
|
ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt_format.item.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()) },
|
||||||
|
head,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -454,42 +497,260 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_human_readable_examples(span: Span) -> Value {
|
fn merge_record(record: &Record, head: Span, span: Span) -> Result<Value, ShellError> {
|
||||||
let examples: Vec<String> = vec![
|
if let Some(invalid_col) = record
|
||||||
"Today 18:30".into(),
|
.columns()
|
||||||
"2022-11-07 13:25:30".into(),
|
.find(|key| !ALLOWED_COLUMNS.contains(&key.as_str()))
|
||||||
"15:20 Friday".into(),
|
{
|
||||||
"This Friday 17:00".into(),
|
let allowed_cols = ALLOWED_COLUMNS.join(", ");
|
||||||
"13:25, Next Tuesday".into(),
|
return Err(ShellError::UnsupportedInput {
|
||||||
"Last Friday at 19:45".into(),
|
msg: format!(
|
||||||
"In 3 days".into(),
|
"Column '{invalid_col}' is not valid for a structured datetime. Allowed columns are: {allowed_cols}"
|
||||||
"In 2 hours".into(),
|
),
|
||||||
"10 hours and 5 minutes ago".into(),
|
input: "value originates from here".into(),
|
||||||
"1 years ago".into(),
|
msg_span: head,
|
||||||
"A year ago".into(),
|
input_span: span
|
||||||
"A month ago".into(),
|
}
|
||||||
"A week ago".into(),
|
);
|
||||||
"A day ago".into(),
|
};
|
||||||
"An hour ago".into(),
|
|
||||||
"A minute ago".into(),
|
|
||||||
"A second ago".into(),
|
|
||||||
"Now".into(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let records = examples
|
// Empty fields are filled in a specific way: the time units bigger than the biggest provided fields are assumed to be current and smaller ones are zeroed.
|
||||||
.iter()
|
// And local timezone is used if not provided.
|
||||||
.map(|s| {
|
#[derive(Debug)]
|
||||||
Value::record(
|
enum RecordColumnDefault {
|
||||||
record! {
|
Now,
|
||||||
"parseable human datetime examples" => Value::test_string(s.to_string()),
|
Zero,
|
||||||
"result" => action(&Value::test_string(s.to_string()), &Arguments { zone_options: None, format_options: None, cell_paths: None }, span)
|
}
|
||||||
},
|
let mut record_column_default = RecordColumnDefault::Now;
|
||||||
span,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<Value>>();
|
|
||||||
|
|
||||||
Value::list(records, span)
|
let now = Local::now();
|
||||||
|
let mut now_nanosecond = now.nanosecond();
|
||||||
|
let now_millisecond = now_nanosecond / 1_000_000;
|
||||||
|
now_nanosecond %= 1_000_000;
|
||||||
|
let now_microsecond = now_nanosecond / 1_000;
|
||||||
|
now_nanosecond %= 1_000;
|
||||||
|
|
||||||
|
let year: i32 = match record.get("year") {
|
||||||
|
Some(val) => {
|
||||||
|
record_column_default = RecordColumnDefault::Zero;
|
||||||
|
match val {
|
||||||
|
Value::Int { val, .. } => *val as i32,
|
||||||
|
other => {
|
||||||
|
return Err(ShellError::OnlySupportsThisInputType {
|
||||||
|
exp_input_type: "int".to_string(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
|
dst_span: head,
|
||||||
|
src_span: other.span(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => now.year(),
|
||||||
|
};
|
||||||
|
let month = match record.get("month") {
|
||||||
|
Some(col_val) => {
|
||||||
|
record_column_default = RecordColumnDefault::Zero;
|
||||||
|
parse_value_from_record_as_u32("month", col_val, &head, &span)?
|
||||||
|
}
|
||||||
|
None => match record_column_default {
|
||||||
|
RecordColumnDefault::Now => now.month(),
|
||||||
|
RecordColumnDefault::Zero => 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let day = match record.get("day") {
|
||||||
|
Some(col_val) => {
|
||||||
|
record_column_default = RecordColumnDefault::Zero;
|
||||||
|
parse_value_from_record_as_u32("day", col_val, &head, &span)?
|
||||||
|
}
|
||||||
|
None => match record_column_default {
|
||||||
|
RecordColumnDefault::Now => now.day(),
|
||||||
|
RecordColumnDefault::Zero => 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let hour = match record.get("hour") {
|
||||||
|
Some(col_val) => {
|
||||||
|
record_column_default = RecordColumnDefault::Zero;
|
||||||
|
parse_value_from_record_as_u32("hour", col_val, &head, &span)?
|
||||||
|
}
|
||||||
|
None => match record_column_default {
|
||||||
|
RecordColumnDefault::Now => now.hour(),
|
||||||
|
RecordColumnDefault::Zero => 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let minute = match record.get("minute") {
|
||||||
|
Some(col_val) => {
|
||||||
|
record_column_default = RecordColumnDefault::Zero;
|
||||||
|
parse_value_from_record_as_u32("minute", col_val, &head, &span)?
|
||||||
|
}
|
||||||
|
None => match record_column_default {
|
||||||
|
RecordColumnDefault::Now => now.minute(),
|
||||||
|
RecordColumnDefault::Zero => 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let second = match record.get("second") {
|
||||||
|
Some(col_val) => {
|
||||||
|
record_column_default = RecordColumnDefault::Zero;
|
||||||
|
parse_value_from_record_as_u32("second", col_val, &head, &span)?
|
||||||
|
}
|
||||||
|
None => match record_column_default {
|
||||||
|
RecordColumnDefault::Now => now.second(),
|
||||||
|
RecordColumnDefault::Zero => 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let millisecond = match record.get("millisecond") {
|
||||||
|
Some(col_val) => {
|
||||||
|
record_column_default = RecordColumnDefault::Zero;
|
||||||
|
parse_value_from_record_as_u32("millisecond", col_val, &head, &span)?
|
||||||
|
}
|
||||||
|
None => match record_column_default {
|
||||||
|
RecordColumnDefault::Now => now_millisecond,
|
||||||
|
RecordColumnDefault::Zero => 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let microsecond = match record.get("microsecond") {
|
||||||
|
Some(col_val) => {
|
||||||
|
record_column_default = RecordColumnDefault::Zero;
|
||||||
|
parse_value_from_record_as_u32("microsecond", col_val, &head, &span)?
|
||||||
|
}
|
||||||
|
None => match record_column_default {
|
||||||
|
RecordColumnDefault::Now => now_microsecond,
|
||||||
|
RecordColumnDefault::Zero => 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let nanosecond = match record.get("nanosecond") {
|
||||||
|
Some(col_val) => parse_value_from_record_as_u32("nanosecond", col_val, &head, &span)?,
|
||||||
|
None => match record_column_default {
|
||||||
|
RecordColumnDefault::Now => now_nanosecond,
|
||||||
|
RecordColumnDefault::Zero => 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset: FixedOffset = match record.get("timezone") {
|
||||||
|
Some(timezone) => parse_timezone_from_record(timezone, &head, &timezone.span())?,
|
||||||
|
None => now.offset().to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let total_nanoseconds = nanosecond + microsecond * 1_000 + millisecond * 1_000_000;
|
||||||
|
|
||||||
|
let date = match NaiveDate::from_ymd_opt(year, month, day) {
|
||||||
|
Some(d) => d,
|
||||||
|
None => {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "one of more values are incorrect and do not represent valid date".to_string(),
|
||||||
|
val_span: head,
|
||||||
|
call_span: span,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let time = match NaiveTime::from_hms_nano_opt(hour, minute, second, total_nanoseconds) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "one of more values are incorrect and do not represent valid time".to_string(),
|
||||||
|
val_span: head,
|
||||||
|
call_span: span,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let date_time = NaiveDateTime::new(date, time);
|
||||||
|
|
||||||
|
let date_time_fixed = match offset.from_local_datetime(&date_time).single() {
|
||||||
|
Some(d) => d,
|
||||||
|
None => {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "Ambiguous or invalid timezone conversion".to_string(),
|
||||||
|
val_span: head,
|
||||||
|
call_span: span,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Value::date(date_time_fixed, span))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_value_from_record_as_u32(
|
||||||
|
col: &str,
|
||||||
|
col_val: &Value,
|
||||||
|
head: &Span,
|
||||||
|
span: &Span,
|
||||||
|
) -> Result<u32, ShellError> {
|
||||||
|
let value: u32 = match col_val {
|
||||||
|
Value::Int { val, .. } => {
|
||||||
|
if *val < 0 || *val > u32::MAX as i64 {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: format!("incorrect value for {}", col),
|
||||||
|
val_span: *head,
|
||||||
|
call_span: *span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*val as u32
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(ShellError::OnlySupportsThisInputType {
|
||||||
|
exp_input_type: "int".to_string(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
|
dst_span: *head,
|
||||||
|
src_span: other.span(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_timezone_from_record(
|
||||||
|
timezone: &Value,
|
||||||
|
head: &Span,
|
||||||
|
span: &Span,
|
||||||
|
) -> Result<FixedOffset, ShellError> {
|
||||||
|
match timezone {
|
||||||
|
Value::String { val, .. } => {
|
||||||
|
let offset: FixedOffset = match val.parse() {
|
||||||
|
Ok(offset) => offset,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "invalid timezone".to_string(),
|
||||||
|
val_span: *span,
|
||||||
|
call_span: *head,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(offset)
|
||||||
|
}
|
||||||
|
other => Err(ShellError::OnlySupportsThisInputType {
|
||||||
|
exp_input_type: "string".to_string(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
|
dst_span: *head,
|
||||||
|
src_span: other.span(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_with_format(val: &str, fmt: &str, head: Span) -> Result<Value, ()> {
|
||||||
|
// try parsing at date + time
|
||||||
|
if let Ok(dt) = NaiveDateTime::parse_from_str(val, fmt) {
|
||||||
|
let dt_native = Local.from_local_datetime(&dt).single().unwrap_or_default();
|
||||||
|
return Ok(Value::date(dt_native.into(), head));
|
||||||
|
}
|
||||||
|
|
||||||
|
// try parsing at date only
|
||||||
|
if let Ok(date) = NaiveDate::parse_from_str(val, fmt) {
|
||||||
|
if let Some(dt) = date.and_hms_opt(0, 0, 0) {
|
||||||
|
let dt_native = Local.from_local_datetime(&dt).single().unwrap_or_default();
|
||||||
|
return Ok(Value::date(dt_native.into(), head));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try parsing at time only
|
||||||
|
if let Ok(time) = NaiveTime::parse_from_str(val, fmt) {
|
||||||
|
let now = Local::now().naive_local().date();
|
||||||
|
let dt_native = Local
|
||||||
|
.from_local_datetime(&now.and_time(time))
|
||||||
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
|
return Ok(Value::date(dt_native.into(), head));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -508,7 +769,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn takes_a_date_format_with_timezone() {
|
fn takes_a_date_format_with_timezone() {
|
||||||
let date_str = Value::test_string("16.11.1984 8:00 am +0000");
|
let date_str = Value::test_string("16.11.1984 8:00 am +0000");
|
||||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
let fmt_options = Some(Spanned {
|
||||||
|
item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
|
||||||
|
span: Span::test_data(),
|
||||||
|
});
|
||||||
let args = Arguments {
|
let args = Arguments {
|
||||||
zone_options: None,
|
zone_options: None,
|
||||||
format_options: fmt_options,
|
format_options: fmt_options,
|
||||||
@ -523,16 +787,12 @@ 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(Spanned {
|
||||||
|
item: DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()),
|
||||||
|
span: Span::test_data(),
|
||||||
|
});
|
||||||
let args = Arguments {
|
let args = Arguments {
|
||||||
zone_options: None,
|
zone_options: None,
|
||||||
format_options: fmt_options,
|
format_options: fmt_options,
|
||||||
@ -614,7 +874,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn takes_int_with_formatstring() {
|
fn takes_int_with_formatstring() {
|
||||||
let date_int = Value::test_int(1_614_434_140);
|
let date_int = Value::test_int(1_614_434_140);
|
||||||
let fmt_options = Some(DatetimeFormat("%s".to_string()));
|
let fmt_options = Some(Spanned {
|
||||||
|
item: DatetimeFormat("%s".to_string()),
|
||||||
|
span: Span::test_data(),
|
||||||
|
});
|
||||||
let args = Arguments {
|
let args = Arguments {
|
||||||
zone_options: None,
|
zone_options: None,
|
||||||
format_options: fmt_options,
|
format_options: fmt_options,
|
||||||
@ -629,6 +892,55 @@ mod tests {
|
|||||||
assert_eq!(actual, expected)
|
assert_eq!(actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn takes_timestamp_offset_as_int_with_formatting() {
|
||||||
|
let date_int = Value::test_int(1_614_434_140);
|
||||||
|
let timezone_option = Some(Spanned {
|
||||||
|
item: Zone::East(8),
|
||||||
|
span: Span::test_data(),
|
||||||
|
});
|
||||||
|
let fmt_options = Some(Spanned {
|
||||||
|
item: DatetimeFormat("%s".to_string()),
|
||||||
|
span: Span::test_data(),
|
||||||
|
});
|
||||||
|
let args = Arguments {
|
||||||
|
zone_options: timezone_option,
|
||||||
|
format_options: fmt_options,
|
||||||
|
cell_paths: None,
|
||||||
|
};
|
||||||
|
let actual = action(&date_int, &args, Span::test_data());
|
||||||
|
let expected = Value::date(
|
||||||
|
DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
|
||||||
|
Span::test_data(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn takes_timestamp_offset_as_int_with_local_timezone() {
|
||||||
|
let date_int = Value::test_int(1_614_434_140);
|
||||||
|
let timezone_option = Some(Spanned {
|
||||||
|
item: Zone::Local,
|
||||||
|
span: Span::test_data(),
|
||||||
|
});
|
||||||
|
let fmt_options = Some(Spanned {
|
||||||
|
item: DatetimeFormat("%s".to_string()),
|
||||||
|
span: Span::test_data(),
|
||||||
|
});
|
||||||
|
let args = Arguments {
|
||||||
|
zone_options: timezone_option,
|
||||||
|
format_options: fmt_options,
|
||||||
|
cell_paths: None,
|
||||||
|
};
|
||||||
|
let actual = action(&date_int, &args, Span::test_data());
|
||||||
|
let expected = Value::date(
|
||||||
|
Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(),
|
||||||
|
Span::test_data(),
|
||||||
|
);
|
||||||
|
assert_eq!(actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn takes_timestamp() {
|
fn takes_timestamp() {
|
||||||
let date_str = Value::test_string("1614434140000000000");
|
let date_str = Value::test_string("1614434140000000000");
|
||||||
@ -643,7 +955,7 @@ 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(
|
||||||
Local.timestamp_opt(1614434140, 0).unwrap().into(),
|
Local.timestamp_opt(1_614_434_140, 0).unwrap().into(),
|
||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -662,7 +974,7 @@ mod tests {
|
|||||||
cell_paths: None,
|
cell_paths: None,
|
||||||
};
|
};
|
||||||
let expected = Value::date(
|
let expected = Value::date(
|
||||||
Local.timestamp_opt(1614434140, 0).unwrap().into(),
|
Local.timestamp_opt(1_614_434_140, 0).unwrap().into(),
|
||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
);
|
);
|
||||||
let actual = action(&expected, &args, Span::test_data());
|
let actual = action(&expected, &args, Span::test_data());
|
||||||
@ -681,7 +993,7 @@ 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(
|
||||||
Utc.timestamp_opt(1614434140, 0).unwrap().into(),
|
Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(),
|
||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -691,7 +1003,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn communicates_parsing_error_given_an_invalid_datetimelike_string() {
|
fn communicates_parsing_error_given_an_invalid_datetimelike_string() {
|
||||||
let date_str = Value::test_string("16.11.1984 8:00 am Oops0000");
|
let date_str = Value::test_string("16.11.1984 8:00 am Oops0000");
|
||||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
let fmt_options = Some(Spanned {
|
||||||
|
item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
|
||||||
|
span: Span::test_data(),
|
||||||
|
});
|
||||||
let args = Arguments {
|
let args = Arguments {
|
||||||
zone_options: None,
|
zone_options: None,
|
||||||
format_options: fmt_options,
|
format_options: fmt_options,
|
||||||
|
@ -1,8 +1,41 @@
|
|||||||
|
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS};
|
use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS};
|
||||||
use nu_protocol::{ast::Expr, Unit};
|
use nu_protocol::{ast::Expr, Unit};
|
||||||
|
|
||||||
|
const NS_PER_US: i64 = 1_000;
|
||||||
|
const NS_PER_MS: i64 = 1_000_000;
|
||||||
const NS_PER_SEC: i64 = 1_000_000_000;
|
const NS_PER_SEC: i64 = 1_000_000_000;
|
||||||
|
const NS_PER_MINUTE: i64 = 60 * NS_PER_SEC;
|
||||||
|
const NS_PER_HOUR: i64 = 60 * NS_PER_MINUTE;
|
||||||
|
const NS_PER_DAY: i64 = 24 * NS_PER_HOUR;
|
||||||
|
const NS_PER_WEEK: i64 = 7 * NS_PER_DAY;
|
||||||
|
|
||||||
|
const ALLOWED_COLUMNS: [&str; 9] = [
|
||||||
|
"week",
|
||||||
|
"day",
|
||||||
|
"hour",
|
||||||
|
"minute",
|
||||||
|
"second",
|
||||||
|
"millisecond",
|
||||||
|
"microsecond",
|
||||||
|
"nanosecond",
|
||||||
|
"sign",
|
||||||
|
];
|
||||||
|
const ALLOWED_SIGNS: [&str; 2] = ["+", "-"];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Arguments {
|
||||||
|
unit: Option<Spanned<String>>,
|
||||||
|
cell_paths: Option<Vec<CellPath>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CmdArgument for Arguments {
|
||||||
|
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
|
||||||
|
self.cell_paths.take()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct IntoDuration;
|
pub struct IntoDuration;
|
||||||
|
|
||||||
@ -15,13 +48,17 @@ impl Command for IntoDuration {
|
|||||||
Signature::build("into duration")
|
Signature::build("into duration")
|
||||||
.input_output_types(vec![
|
.input_output_types(vec![
|
||||||
(Type::Int, Type::Duration),
|
(Type::Int, Type::Duration),
|
||||||
|
(Type::Float, Type::Duration),
|
||||||
(Type::String, Type::Duration),
|
(Type::String, Type::Duration),
|
||||||
(Type::Duration, Type::Duration),
|
(Type::Duration, Type::Duration),
|
||||||
|
// FIXME: https://github.com/nushell/nushell/issues/15485
|
||||||
|
// 'record -> any' was added as a temporary workaround to avoid type inference issues. The Any arm needs to be appear first.
|
||||||
|
(Type::record(), Type::Any),
|
||||||
|
(Type::record(), Type::record()),
|
||||||
|
(Type::record(), Type::Duration),
|
||||||
(Type::table(), Type::table()),
|
(Type::table(), Type::table()),
|
||||||
//todo: record<hour,minute,sign> | into duration -> Duration
|
|
||||||
//(Type::record(), Type::record()),
|
|
||||||
])
|
])
|
||||||
//.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.named(
|
.named(
|
||||||
"unit",
|
"unit",
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
@ -55,7 +92,35 @@ impl Command for IntoDuration {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
into_duration(engine_state, stack, call, input)
|
let cell_paths = call.rest(engine_state, stack, 0)?;
|
||||||
|
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||||
|
|
||||||
|
let span = match input.span() {
|
||||||
|
Some(t) => t,
|
||||||
|
None => call.head,
|
||||||
|
};
|
||||||
|
let unit = match call.get_flag::<Spanned<String>>(engine_state, stack, "unit")? {
|
||||||
|
Some(spanned_unit) => {
|
||||||
|
if ["ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk"]
|
||||||
|
.contains(&spanned_unit.item.as_str())
|
||||||
|
{
|
||||||
|
Some(spanned_unit)
|
||||||
|
} else {
|
||||||
|
return Err(ShellError::CantConvertToDuration {
|
||||||
|
details: spanned_unit.item,
|
||||||
|
dst_span: span,
|
||||||
|
src_span: span,
|
||||||
|
help: Some(
|
||||||
|
"supported units are ns, us/µs, ms, sec, min, hr, day, and wk"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let args = Arguments { unit, cell_paths };
|
||||||
|
operate(action, args, input, call.head, engine_state.signals())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
@ -109,67 +174,23 @@ impl Command for IntoDuration {
|
|||||||
example: "1_234 | into duration --unit ms",
|
example: "1_234 | into duration --unit ms",
|
||||||
result: Some(Value::test_duration(1_234 * 1_000_000)),
|
result: Some(Value::test_duration(1_234 * 1_000_000)),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Convert a floating point number of an arbitrary unit to duration",
|
||||||
|
example: "1.234 | into duration --unit sec",
|
||||||
|
result: Some(Value::test_duration(1_234 * 1_000_000)),
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Convert a record to a duration",
|
||||||
|
example: "{day: 10, hour: 2, minute: 6, second: 50, sign: '+'} | into duration",
|
||||||
|
result: Some(Value::duration(
|
||||||
|
10 * NS_PER_DAY + 2 * NS_PER_HOUR + 6 * NS_PER_MINUTE + 50 * NS_PER_SEC,
|
||||||
|
Span::test_data(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_duration(
|
|
||||||
engine_state: &EngineState,
|
|
||||||
stack: &mut Stack,
|
|
||||||
call: &Call,
|
|
||||||
input: PipelineData,
|
|
||||||
) -> Result<PipelineData, ShellError> {
|
|
||||||
let span = match input.span() {
|
|
||||||
Some(t) => t,
|
|
||||||
None => call.head,
|
|
||||||
};
|
|
||||||
let column_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
|
|
||||||
|
|
||||||
let unit = match call.get_flag::<String>(engine_state, stack, "unit")? {
|
|
||||||
Some(sep) => {
|
|
||||||
if ["ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk"]
|
|
||||||
.iter()
|
|
||||||
.any(|d| d == &sep)
|
|
||||||
{
|
|
||||||
sep
|
|
||||||
} else {
|
|
||||||
return Err(ShellError::CantConvertToDuration {
|
|
||||||
details: sep,
|
|
||||||
dst_span: span,
|
|
||||||
src_span: span,
|
|
||||||
help: Some(
|
|
||||||
"supported units are ns, us/µs, ms, sec, min, hr, day, and wk".to_string(),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => "ns".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
input.map(
|
|
||||||
move |v| {
|
|
||||||
if column_paths.is_empty() {
|
|
||||||
action(&v, &unit.clone(), span)
|
|
||||||
} else {
|
|
||||||
let unitclone = &unit.clone();
|
|
||||||
let mut ret = v;
|
|
||||||
for path in &column_paths {
|
|
||||||
let r = ret.update_cell_path(
|
|
||||||
&path.members,
|
|
||||||
Box::new(move |old| action(old, unitclone, span)),
|
|
||||||
);
|
|
||||||
if let Err(error) = r {
|
|
||||||
return Value::error(error, span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
},
|
|
||||||
engine_state.signals(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_whitespace_indices(s: &str, span: Span) -> impl Iterator<Item = (&str, Span)> {
|
fn split_whitespace_indices(s: &str, span: Span) -> impl Iterator<Item = (&str, Span)> {
|
||||||
s.split_whitespace().map(move |sub| {
|
s.split_whitespace().map(move |sub| {
|
||||||
// Gets the offset of the `sub` substring inside the string `s`.
|
// Gets the offset of the `sub` substring inside the string `s`.
|
||||||
@ -232,27 +253,51 @@ fn string_to_duration(s: &str, span: Span) -> Result<i64, ShellError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action(input: &Value, unit: &str, span: Span) -> Value {
|
fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
||||||
let value_span = input.span();
|
let value_span = input.span();
|
||||||
|
let unit_option = &args.unit;
|
||||||
|
|
||||||
|
if let Value::Record { .. } | Value::Duration { .. } = input {
|
||||||
|
if let Some(unit) = unit_option {
|
||||||
|
return Value::error(
|
||||||
|
ShellError::IncompatibleParameters {
|
||||||
|
left_message: "got a record as input".into(),
|
||||||
|
left_span: head,
|
||||||
|
right_message: "the units should be included in the record".into(),
|
||||||
|
right_span: unit.span,
|
||||||
|
},
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let unit: &str = match unit_option {
|
||||||
|
Some(unit) => &unit.item,
|
||||||
|
None => "ns",
|
||||||
|
};
|
||||||
|
|
||||||
match input {
|
match input {
|
||||||
Value::Duration { .. } => input.clone(),
|
Value::Duration { .. } => input.clone(),
|
||||||
Value::String { val, .. } => match compound_to_duration(val, value_span) {
|
Value::Record { val, .. } => {
|
||||||
Ok(val) => Value::duration(val, span),
|
merge_record(val, head, value_span).unwrap_or_else(|err| Value::error(err, value_span))
|
||||||
Err(error) => Value::error(error, span),
|
}
|
||||||
},
|
Value::String { val, .. } => {
|
||||||
|
if let Ok(num) = val.parse::<f64>() {
|
||||||
|
let ns = unit_to_ns_factor(unit);
|
||||||
|
return Value::duration((num * (ns as f64)) as i64, head);
|
||||||
|
}
|
||||||
|
match compound_to_duration(val, value_span) {
|
||||||
|
Ok(val) => Value::duration(val, head),
|
||||||
|
Err(error) => Value::error(error, head),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Float { val, .. } => {
|
||||||
|
let ns = unit_to_ns_factor(unit);
|
||||||
|
Value::duration((*val * (ns as f64)) as i64, head)
|
||||||
|
}
|
||||||
Value::Int { val, .. } => {
|
Value::Int { val, .. } => {
|
||||||
let ns = match unit {
|
let ns = unit_to_ns_factor(unit);
|
||||||
"ns" => 1,
|
Value::duration(*val * ns, head)
|
||||||
"us" | "µs" => 1_000,
|
|
||||||
"ms" => 1_000_000,
|
|
||||||
"sec" => NS_PER_SEC,
|
|
||||||
"min" => NS_PER_SEC * 60,
|
|
||||||
"hr" => NS_PER_SEC * 60 * 60,
|
|
||||||
"day" => NS_PER_SEC * 60 * 60 * 24,
|
|
||||||
"wk" => NS_PER_SEC * 60 * 60 * 24 * 7,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
Value::duration(*val * ns, span)
|
|
||||||
}
|
}
|
||||||
// Propagate errors by explicitly matching them before the final case.
|
// Propagate errors by explicitly matching them before the final case.
|
||||||
Value::Error { .. } => input.clone(),
|
Value::Error { .. } => input.clone(),
|
||||||
@ -260,14 +305,134 @@ fn action(input: &Value, unit: &str, span: Span) -> Value {
|
|||||||
ShellError::OnlySupportsThisInputType {
|
ShellError::OnlySupportsThisInputType {
|
||||||
exp_input_type: "string or duration".into(),
|
exp_input_type: "string or duration".into(),
|
||||||
wrong_type: other.get_type().to_string(),
|
wrong_type: other.get_type().to_string(),
|
||||||
dst_span: span,
|
dst_span: head,
|
||||||
src_span: other.span(),
|
src_span: other.span(),
|
||||||
},
|
},
|
||||||
span,
|
head,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn merge_record(record: &Record, head: Span, span: Span) -> Result<Value, ShellError> {
|
||||||
|
if let Some(invalid_col) = record
|
||||||
|
.columns()
|
||||||
|
.find(|key| !ALLOWED_COLUMNS.contains(&key.as_str()))
|
||||||
|
{
|
||||||
|
let allowed_cols = ALLOWED_COLUMNS.join(", ");
|
||||||
|
return Err(ShellError::UnsupportedInput {
|
||||||
|
msg: format!(
|
||||||
|
"Column '{invalid_col}' is not valid for a structured duration. Allowed columns are: {allowed_cols}"
|
||||||
|
),
|
||||||
|
input: "value originates from here".into(),
|
||||||
|
msg_span: head,
|
||||||
|
input_span: span
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut duration: i64 = 0;
|
||||||
|
|
||||||
|
if let Some(col_val) = record.get("week") {
|
||||||
|
let week = parse_number_from_record(col_val, &head)?;
|
||||||
|
duration += week * NS_PER_WEEK;
|
||||||
|
};
|
||||||
|
if let Some(col_val) = record.get("day") {
|
||||||
|
let day = parse_number_from_record(col_val, &head)?;
|
||||||
|
duration += day * NS_PER_DAY;
|
||||||
|
};
|
||||||
|
if let Some(col_val) = record.get("hour") {
|
||||||
|
let hour = parse_number_from_record(col_val, &head)?;
|
||||||
|
duration += hour * NS_PER_HOUR;
|
||||||
|
};
|
||||||
|
if let Some(col_val) = record.get("minute") {
|
||||||
|
let minute = parse_number_from_record(col_val, &head)?;
|
||||||
|
duration += minute * NS_PER_MINUTE;
|
||||||
|
};
|
||||||
|
if let Some(col_val) = record.get("second") {
|
||||||
|
let second = parse_number_from_record(col_val, &head)?;
|
||||||
|
duration += second * NS_PER_SEC;
|
||||||
|
};
|
||||||
|
if let Some(col_val) = record.get("millisecond") {
|
||||||
|
let millisecond = parse_number_from_record(col_val, &head)?;
|
||||||
|
duration += millisecond * NS_PER_MS;
|
||||||
|
};
|
||||||
|
if let Some(col_val) = record.get("microsecond") {
|
||||||
|
let microsecond = parse_number_from_record(col_val, &head)?;
|
||||||
|
duration += microsecond * NS_PER_US;
|
||||||
|
};
|
||||||
|
if let Some(col_val) = record.get("nanosecond") {
|
||||||
|
let nanosecond = parse_number_from_record(col_val, &head)?;
|
||||||
|
duration += nanosecond;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(sign) = record.get("sign") {
|
||||||
|
match sign {
|
||||||
|
Value::String { val, .. } => {
|
||||||
|
if !ALLOWED_SIGNS.contains(&val.as_str()) {
|
||||||
|
let allowed_signs = ALLOWED_SIGNS.join(", ");
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: format!("Invalid sign. Allowed signs are {}", allowed_signs)
|
||||||
|
.to_string(),
|
||||||
|
val_span: sign.span(),
|
||||||
|
call_span: head,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if val == "-" {
|
||||||
|
duration = -duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(ShellError::OnlySupportsThisInputType {
|
||||||
|
exp_input_type: "int".to_string(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
|
dst_span: head,
|
||||||
|
src_span: other.span(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::duration(duration, span))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_number_from_record(col_val: &Value, head: &Span) -> Result<i64, ShellError> {
|
||||||
|
let value = match col_val {
|
||||||
|
Value::Int { val, .. } => {
|
||||||
|
if *val < 0 {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "number should be positive".to_string(),
|
||||||
|
val_span: col_val.span(),
|
||||||
|
call_span: *head,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*val
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(ShellError::OnlySupportsThisInputType {
|
||||||
|
exp_input_type: "int".to_string(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
|
dst_span: *head,
|
||||||
|
src_span: other.span(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unit_to_ns_factor(unit: &str) -> i64 {
|
||||||
|
match unit {
|
||||||
|
"ns" => 1,
|
||||||
|
"us" | "µs" => NS_PER_US,
|
||||||
|
"ms" => NS_PER_MS,
|
||||||
|
"sec" => NS_PER_SEC,
|
||||||
|
"min" => NS_PER_MINUTE,
|
||||||
|
"hr" => NS_PER_HOUR,
|
||||||
|
"day" => NS_PER_DAY,
|
||||||
|
"wk" => NS_PER_WEEK,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -284,24 +449,27 @@ mod test {
|
|||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case("3ns", 3)]
|
#[case("3ns", 3)]
|
||||||
#[case("4us", 4*1000)]
|
#[case("4us", 4 * NS_PER_US)]
|
||||||
#[case("4\u{00B5}s", 4*1000)] // micro sign
|
#[case("4\u{00B5}s", 4 * NS_PER_US)] // micro sign
|
||||||
#[case("4\u{03BC}s", 4*1000)] // mu symbol
|
#[case("4\u{03BC}s", 4 * NS_PER_US)] // mu symbol
|
||||||
#[case("5ms", 5 * 1000 * 1000)]
|
#[case("5ms", 5 * NS_PER_MS)]
|
||||||
#[case("1sec", NS_PER_SEC)]
|
#[case("1sec", NS_PER_SEC)]
|
||||||
#[case("7min", 7 * 60 * NS_PER_SEC)]
|
#[case("7min", 7 * NS_PER_MINUTE)]
|
||||||
#[case("42hr", 42 * 60 * 60 * NS_PER_SEC)]
|
#[case("42hr", 42 * NS_PER_HOUR)]
|
||||||
#[case("123day", 123 * 24 * 60 * 60 * NS_PER_SEC)]
|
#[case("123day", 123 * NS_PER_DAY)]
|
||||||
#[case("3wk", 3 * 7 * 24 * 60 * 60 * NS_PER_SEC)]
|
#[case("3wk", 3 * NS_PER_WEEK)]
|
||||||
#[case("86hr 26ns", 86 * 3600 * NS_PER_SEC + 26)] // compound duration string
|
#[case("86hr 26ns", 86 * 3600 * NS_PER_SEC + 26)] // compound duration string
|
||||||
#[case("14ns 3hr 17sec", 14 + 3 * 3600 * NS_PER_SEC + 17 * NS_PER_SEC)] // compound string with units in random order
|
#[case("14ns 3hr 17sec", 14 + 3 * NS_PER_HOUR + 17 * NS_PER_SEC)] // compound string with units in random order
|
||||||
|
|
||||||
fn turns_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) {
|
fn turns_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) {
|
||||||
let actual = action(
|
let args = Arguments {
|
||||||
&Value::test_string(phrase),
|
unit: Some(Spanned {
|
||||||
"ns",
|
item: "ns".to_string(),
|
||||||
Span::new(0, phrase.len()),
|
span: Span::test_data(),
|
||||||
);
|
}),
|
||||||
|
cell_paths: None,
|
||||||
|
};
|
||||||
|
let actual = action(&Value::test_string(phrase), &args, Span::test_data());
|
||||||
match actual {
|
match actual {
|
||||||
Value::Duration {
|
Value::Duration {
|
||||||
val: observed_val, ..
|
val: observed_val, ..
|
||||||
|
@ -208,7 +208,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
|||||||
}
|
}
|
||||||
} else if DATETIME_DMY_RE.is_match(&val_str).unwrap_or(false) {
|
} else if DATETIME_DMY_RE.is_match(&val_str).unwrap_or(false) {
|
||||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||||
to_type: "date".to_string(),
|
to_type: "datetime".to_string(),
|
||||||
from_type: "string".to_string(),
|
from_type: "string".to_string(),
|
||||||
span,
|
span,
|
||||||
help: Some(format!(
|
help: Some(format!(
|
||||||
@ -219,7 +219,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
|||||||
Ok(Value::date(dt, span))
|
Ok(Value::date(dt, span))
|
||||||
} else if DATETIME_YMD_RE.is_match(&val_str).unwrap_or(false) {
|
} else if DATETIME_YMD_RE.is_match(&val_str).unwrap_or(false) {
|
||||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||||
to_type: "date".to_string(),
|
to_type: "datetime".to_string(),
|
||||||
from_type: "string".to_string(),
|
from_type: "string".to_string(),
|
||||||
span,
|
span,
|
||||||
help: Some(format!(
|
help: Some(format!(
|
||||||
@ -230,7 +230,7 @@ fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Va
|
|||||||
Ok(Value::date(dt, span))
|
Ok(Value::date(dt, span))
|
||||||
} else if DATETIME_YMDZ_RE.is_match(&val_str).unwrap_or(false) {
|
} else if DATETIME_YMDZ_RE.is_match(&val_str).unwrap_or(false) {
|
||||||
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
||||||
to_type: "date".to_string(),
|
to_type: "datetime".to_string(),
|
||||||
from_type: "string".to_string(),
|
from_type: "string".to_string(),
|
||||||
span,
|
span,
|
||||||
help: Some(format!(
|
help: Some(format!(
|
||||||
|
@ -40,6 +40,7 @@ impl Command for SplitCellPath {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
|
let input_type = input.get_type();
|
||||||
|
|
||||||
let src_span = match input {
|
let src_span = match input {
|
||||||
// Early return on correct type and empty pipeline
|
// Early return on correct type and empty pipeline
|
||||||
@ -54,8 +55,9 @@ impl Command for SplitCellPath {
|
|||||||
PipelineData::ListStream(stream, ..) => stream.span(),
|
PipelineData::ListStream(stream, ..) => stream.span(),
|
||||||
PipelineData::ByteStream(stream, ..) => stream.span(),
|
PipelineData::ByteStream(stream, ..) => stream.span(),
|
||||||
};
|
};
|
||||||
Err(ShellError::PipelineMismatch {
|
Err(ShellError::OnlySupportsThisInputType {
|
||||||
exp_input_type: "cell-path".into(),
|
exp_input_type: "cell-path".into(),
|
||||||
|
wrong_type: input_type.to_string(),
|
||||||
dst_span: head,
|
dst_span: head,
|
||||||
src_span,
|
src_span,
|
||||||
})
|
})
|
||||||
|
261
crates/nu-command/src/date/from_human.rs
Normal file
261
crates/nu-command/src/date/from_human.rs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
use chrono::{Local, TimeZone};
|
||||||
|
use human_date_parser::{from_human_time, ParseResult};
|
||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DateFromHuman;
|
||||||
|
|
||||||
|
impl Command for DateFromHuman {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"date from-human"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build("date from-human")
|
||||||
|
.input_output_types(vec![
|
||||||
|
(Type::String, Type::Date),
|
||||||
|
(Type::Nothing, Type::table()),
|
||||||
|
])
|
||||||
|
.allow_variants_without_examples(true)
|
||||||
|
.switch(
|
||||||
|
"list",
|
||||||
|
"Show human-readable datetime parsing examples",
|
||||||
|
Some('l'),
|
||||||
|
)
|
||||||
|
.category(Category::Date)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Convert a human readable datetime string to a datetime."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec![
|
||||||
|
"relative",
|
||||||
|
"now",
|
||||||
|
"today",
|
||||||
|
"tomorrow",
|
||||||
|
"yesterday",
|
||||||
|
"weekday",
|
||||||
|
"weekday_name",
|
||||||
|
"timezone",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
if call.has_flag(engine_state, stack, "list")? {
|
||||||
|
return Ok(list_human_readable_examples(call.head).into_pipeline_data());
|
||||||
|
}
|
||||||
|
let head = call.head;
|
||||||
|
// This doesn't match explicit nulls
|
||||||
|
if matches!(input, PipelineData::Empty) {
|
||||||
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
|
}
|
||||||
|
input.map(move |value| helper(value, head), engine_state.signals())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
description: "Parsing human readable datetime",
|
||||||
|
example: "'Today at 18:30' | date from-human",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Parsing human readable datetime",
|
||||||
|
example: "'Last Friday at 19:45' | date from-human",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Parsing human readable datetime",
|
||||||
|
example: "'In 5 minutes and 30 seconds' | date from-human",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "PShow human-readable datetime parsing examples",
|
||||||
|
example: "date from-human --list",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn helper(value: Value, head: Span) -> Value {
|
||||||
|
let span = value.span();
|
||||||
|
let input_val = match value {
|
||||||
|
Value::String { val, .. } => val,
|
||||||
|
other => {
|
||||||
|
return Value::error(
|
||||||
|
ShellError::OnlySupportsThisInputType {
|
||||||
|
exp_input_type: "string".to_string(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
|
dst_span: head,
|
||||||
|
src_span: span,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Local::now();
|
||||||
|
|
||||||
|
if let Ok(date) = from_human_time(&input_val, now.naive_local()) {
|
||||||
|
match date {
|
||||||
|
ParseResult::Date(date) => {
|
||||||
|
let time = now.time();
|
||||||
|
let combined = date.and_time(time);
|
||||||
|
let local_offset = *now.offset();
|
||||||
|
let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
|
return Value::date(dt_fixed, span);
|
||||||
|
}
|
||||||
|
ParseResult::DateTime(date) => {
|
||||||
|
let local_offset = *now.offset();
|
||||||
|
let dt_fixed = match local_offset.from_local_datetime(&date) {
|
||||||
|
chrono::LocalResult::Single(dt) => dt,
|
||||||
|
chrono::LocalResult::Ambiguous(_, _) => {
|
||||||
|
return Value::error(
|
||||||
|
ShellError::DatetimeParseError {
|
||||||
|
msg: "Ambiguous datetime".to_string(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
chrono::LocalResult::None => {
|
||||||
|
return Value::error(
|
||||||
|
ShellError::DatetimeParseError {
|
||||||
|
msg: "Invalid datetime".to_string(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Value::date(dt_fixed, span);
|
||||||
|
}
|
||||||
|
ParseResult::Time(time) => {
|
||||||
|
let date = now.date_naive();
|
||||||
|
let combined = date.and_time(time);
|
||||||
|
let local_offset = *now.offset();
|
||||||
|
let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
|
return Value::date(dt_fixed, span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match from_human_time(&input_val, now.naive_local()) {
|
||||||
|
Ok(date) => match date {
|
||||||
|
ParseResult::Date(date) => {
|
||||||
|
let time = now.time();
|
||||||
|
let combined = date.and_time(time);
|
||||||
|
let local_offset = *now.offset();
|
||||||
|
let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
|
Value::date(dt_fixed, span)
|
||||||
|
}
|
||||||
|
ParseResult::DateTime(date) => {
|
||||||
|
let local_offset = *now.offset();
|
||||||
|
let dt_fixed = match local_offset.from_local_datetime(&date) {
|
||||||
|
chrono::LocalResult::Single(dt) => dt,
|
||||||
|
chrono::LocalResult::Ambiguous(_, _) => {
|
||||||
|
return Value::error(
|
||||||
|
ShellError::DatetimeParseError {
|
||||||
|
msg: "Ambiguous datetime".to_string(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
chrono::LocalResult::None => {
|
||||||
|
return Value::error(
|
||||||
|
ShellError::DatetimeParseError {
|
||||||
|
msg: "Invalid datetime".to_string(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Value::date(dt_fixed, span)
|
||||||
|
}
|
||||||
|
ParseResult::Time(time) => {
|
||||||
|
let date = now.date_naive();
|
||||||
|
let combined = date.and_time(time);
|
||||||
|
let local_offset = *now.offset();
|
||||||
|
let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_default();
|
||||||
|
Value::date(dt_fixed, span)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => Value::error(
|
||||||
|
ShellError::IncorrectValue {
|
||||||
|
msg: "Cannot parse as humanized date".to_string(),
|
||||||
|
val_span: head,
|
||||||
|
call_span: span,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_human_readable_examples(span: Span) -> Value {
|
||||||
|
let examples: Vec<String> = vec![
|
||||||
|
"Today 18:30".into(),
|
||||||
|
"2022-11-07 13:25:30".into(),
|
||||||
|
"15:20 Friday".into(),
|
||||||
|
"This Friday 17:00".into(),
|
||||||
|
"13:25, Next Tuesday".into(),
|
||||||
|
"Last Friday at 19:45".into(),
|
||||||
|
"In 3 days".into(),
|
||||||
|
"In 2 hours".into(),
|
||||||
|
"10 hours and 5 minutes ago".into(),
|
||||||
|
"1 years ago".into(),
|
||||||
|
"A year ago".into(),
|
||||||
|
"A month ago".into(),
|
||||||
|
"A week ago".into(),
|
||||||
|
"A day ago".into(),
|
||||||
|
"An hour ago".into(),
|
||||||
|
"A minute ago".into(),
|
||||||
|
"A second ago".into(),
|
||||||
|
"Now".into(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let records = examples
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
"parseable human datetime examples" => Value::test_string(s.to_string()),
|
||||||
|
"result" => helper(Value::test_string(s.to_string()), span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Value>>();
|
||||||
|
|
||||||
|
Value::list(records, span)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_examples() {
|
||||||
|
use crate::test_examples;
|
||||||
|
|
||||||
|
test_examples(DateFromHuman {})
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
mod date_;
|
mod date_;
|
||||||
|
mod from_human;
|
||||||
mod humanize;
|
mod humanize;
|
||||||
mod list_timezone;
|
mod list_timezone;
|
||||||
mod now;
|
mod now;
|
||||||
@ -7,6 +8,7 @@ mod to_timezone;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use date_::Date;
|
pub use date_::Date;
|
||||||
|
pub use from_human::DateFromHuman;
|
||||||
pub use humanize::DateHumanize;
|
pub use humanize::DateHumanize;
|
||||||
pub use list_timezone::DateListTimezones;
|
pub use list_timezone::DateListTimezones;
|
||||||
pub use now::DateNow;
|
pub use now::DateNow;
|
||||||
|
@ -38,10 +38,16 @@ impl Command for DateNow {
|
|||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
Example {
|
Example {
|
||||||
description: "Get the current date and display it in a given format string.",
|
description: "Get the current date and format it in a given format string.",
|
||||||
example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#,
|
example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#,
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description:
|
||||||
|
"Get the current date and format it according to the RFC 3339 standard.",
|
||||||
|
example: r#"date now | format date "%+""#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Get the time duration since 2019-04-30.",
|
description: "Get the time duration since 2019-04-30.",
|
||||||
example: r#"(date now) - 2019-05-01"#,
|
example: r#"(date now) - 2019-05-01"#,
|
||||||
@ -53,7 +59,8 @@ impl Command for DateNow {
|
|||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Get current time in full RFC 3339 format with time zone.",
|
description:
|
||||||
|
"Get current time and format it in the debug format (RFC 2822 with timezone)",
|
||||||
example: r#"date now | debug"#,
|
example: r#"date now | debug"#,
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,11 @@ pub(crate) fn parse_date_from_string(
|
|||||||
Ok((native_dt, fixed_offset)) => {
|
Ok((native_dt, fixed_offset)) => {
|
||||||
let offset = match fixed_offset {
|
let offset = match fixed_offset {
|
||||||
Some(offset) => offset,
|
Some(offset) => offset,
|
||||||
None => *(Local::now().offset()),
|
None => *Local
|
||||||
|
.from_local_datetime(&native_dt)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.offset(),
|
||||||
};
|
};
|
||||||
match offset.from_local_datetime(&native_dt) {
|
match offset.from_local_datetime(&native_dt) {
|
||||||
LocalResult::Single(d) => Ok(d),
|
LocalResult::Single(d) => Ok(d),
|
||||||
|
@ -23,6 +23,11 @@ impl Command for Debug {
|
|||||||
])
|
])
|
||||||
.category(Category::Debug)
|
.category(Category::Debug)
|
||||||
.switch("raw", "Prints the raw value representation", Some('r'))
|
.switch("raw", "Prints the raw value representation", Some('r'))
|
||||||
|
.switch(
|
||||||
|
"raw-value",
|
||||||
|
"Prints the raw value representation but not the nushell value part",
|
||||||
|
Some('v'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
@ -35,6 +40,7 @@ impl Command for Debug {
|
|||||||
let head = call.head;
|
let head = call.head;
|
||||||
let config = stack.get_config(engine_state);
|
let config = stack.get_config(engine_state);
|
||||||
let raw = call.has_flag(engine_state, stack, "raw")?;
|
let raw = call.has_flag(engine_state, stack, "raw")?;
|
||||||
|
let raw_value = call.has_flag(engine_state, stack, "raw-value")?;
|
||||||
|
|
||||||
// Should PipelineData::Empty result in an error here?
|
// Should PipelineData::Empty result in an error here?
|
||||||
|
|
||||||
@ -42,6 +48,11 @@ impl Command for Debug {
|
|||||||
move |x| {
|
move |x| {
|
||||||
if raw {
|
if raw {
|
||||||
Value::string(x.to_debug_string(), head)
|
Value::string(x.to_debug_string(), head)
|
||||||
|
} else if raw_value {
|
||||||
|
match x.coerce_into_string_all() {
|
||||||
|
Ok(s) => Value::string(format!("{s:#?}"), head),
|
||||||
|
Err(e) => Value::error(e, head),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Value::string(x.to_expanded_string(", ", &config), head)
|
Value::string(x.to_expanded_string(", ", &config), head)
|
||||||
}
|
}
|
||||||
@ -78,10 +89,75 @@ impl Command for Debug {
|
|||||||
Span::test_data(),
|
Span::test_data(),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Debug print an ansi escape encoded string and get the raw value",
|
||||||
|
example: "$'(ansi red)nushell(ansi reset)' | debug -v",
|
||||||
|
result: Some(Value::test_string("\"\\u{1b}[31mnushell\\u{1b}[0m\"")),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is just a local Value Extension trait to avoid having to
|
||||||
|
// put another *_to_string() converter in nu_protocol
|
||||||
|
trait ValueExt {
|
||||||
|
fn coerce_into_string_all(&self) -> Result<String, ShellError>;
|
||||||
|
fn cant_convert_to<T>(&self, typ: &str) -> Result<T, ShellError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueExt for Value {
|
||||||
|
fn cant_convert_to<T>(&self, typ: &str) -> Result<T, ShellError> {
|
||||||
|
Err(ShellError::CantConvert {
|
||||||
|
to_type: typ.into(),
|
||||||
|
from_type: self.get_type().to_string(),
|
||||||
|
span: self.span(),
|
||||||
|
help: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coerce_into_string_all(&self) -> Result<String, ShellError> {
|
||||||
|
let span = self.span();
|
||||||
|
match self {
|
||||||
|
Value::Bool { val, .. } => Ok(val.to_string()),
|
||||||
|
Value::Int { val, .. } => Ok(val.to_string()),
|
||||||
|
Value::Float { val, .. } => Ok(val.to_string()),
|
||||||
|
Value::String { val, .. } => Ok(val.to_string()),
|
||||||
|
Value::Glob { val, .. } => Ok(val.to_string()),
|
||||||
|
Value::Filesize { val, .. } => Ok(val.get().to_string()),
|
||||||
|
Value::Duration { val, .. } => Ok(val.to_string()),
|
||||||
|
Value::Date { val, .. } => Ok(val.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true)),
|
||||||
|
Value::Range { val, .. } => Ok(val.to_string()),
|
||||||
|
Value::Record { val, .. } => Ok(format!(
|
||||||
|
"{{{}}}",
|
||||||
|
val.iter()
|
||||||
|
.map(|(x, y)| match y.coerce_into_string_all() {
|
||||||
|
Ok(value) => format!("{x}: {value}"),
|
||||||
|
Err(err) => format!("Error: {err}"),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)),
|
||||||
|
Value::List { vals, .. } => Ok(format!(
|
||||||
|
"[{}]",
|
||||||
|
vals.iter()
|
||||||
|
.map(|x| match x.coerce_into_string_all() {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => format!("Error: {err}"),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)),
|
||||||
|
Value::Binary { val, .. } => match String::from_utf8(val.to_vec()) {
|
||||||
|
Ok(s) => Ok(s),
|
||||||
|
Err(err) => Value::binary(err.into_bytes(), span).cant_convert_to("string"),
|
||||||
|
},
|
||||||
|
Value::CellPath { val, .. } => Ok(val.to_string()),
|
||||||
|
Value::Nothing { .. } => Ok("nothing".to_string()),
|
||||||
|
val => val.cant_convert_to("string"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -118,7 +118,7 @@ fn increase_string_width(text: &mut String, total: usize) {
|
|||||||
let rest = total - width;
|
let rest = total - width;
|
||||||
|
|
||||||
if rest > 0 {
|
if rest > 0 {
|
||||||
text.extend(std::iter::repeat(' ').take(rest));
|
text.extend(std::iter::repeat_n(' ', rest));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,6 +272,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
// Date
|
// Date
|
||||||
bind_command! {
|
bind_command! {
|
||||||
Date,
|
Date,
|
||||||
|
DateFromHuman,
|
||||||
DateHumanize,
|
DateHumanize,
|
||||||
DateListTimezones,
|
DateListTimezones,
|
||||||
DateNow,
|
DateNow,
|
||||||
@ -451,9 +452,18 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
JobSpawn,
|
JobSpawn,
|
||||||
JobList,
|
JobList,
|
||||||
JobKill,
|
JobKill,
|
||||||
|
JobId,
|
||||||
|
JobTag,
|
||||||
Job,
|
Job,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
bind_command! {
|
||||||
|
JobSend,
|
||||||
|
JobRecv,
|
||||||
|
JobFlush,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(unix, feature = "os"))]
|
#[cfg(all(unix, feature = "os"))]
|
||||||
bind_command! {
|
bind_command! {
|
||||||
JobUnfreeze,
|
JobUnfreeze,
|
||||||
|
16
crates/nu-command/src/env/config/config_.rs
vendored
16
crates/nu-command/src/env/config/config_.rs
vendored
@ -1,7 +1,11 @@
|
|||||||
use nu_cmd_base::util::get_editor;
|
use nu_cmd_base::util::get_editor;
|
||||||
use nu_engine::{command_prelude::*, env_to_strings, get_full_help};
|
use nu_engine::{command_prelude::*, env_to_strings, get_full_help};
|
||||||
|
use nu_protocol::shell_error::io::IoError;
|
||||||
use nu_system::ForegroundChild;
|
use nu_system::ForegroundChild;
|
||||||
|
|
||||||
|
#[cfg(feature = "os")]
|
||||||
|
use nu_protocol::process::PostWaitCallback;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigMeta;
|
pub struct ConfigMeta;
|
||||||
|
|
||||||
@ -61,7 +65,6 @@ pub(super) fn start_editor(
|
|||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
// Find the editor executable.
|
// Find the editor executable.
|
||||||
|
|
||||||
use nu_protocol::shell_error::io::IoError;
|
|
||||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||||
let cwd = engine_state.cwd(Some(stack))?;
|
let cwd = engine_state.cwd(Some(stack))?;
|
||||||
@ -119,8 +122,17 @@ pub(super) fn start_editor(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let post_wait_callback = PostWaitCallback::for_job_control(engine_state, None, None);
|
||||||
|
|
||||||
// Wrap the output into a `PipelineData::ByteStream`.
|
// Wrap the output into a `PipelineData::ByteStream`.
|
||||||
let child = nu_protocol::process::ChildProcess::new(child, None, false, call.head, None)?;
|
let child = nu_protocol::process::ChildProcess::new(
|
||||||
|
child,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
call.head,
|
||||||
|
Some(post_wait_callback),
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(PipelineData::ByteStream(
|
Ok(PipelineData::ByteStream(
|
||||||
ByteStream::child(child, call.head),
|
ByteStream::child(child, call.head),
|
||||||
None,
|
None,
|
||||||
|
@ -10,7 +10,7 @@ impl Command for Job {
|
|||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("job")
|
Signature::build("job")
|
||||||
.category(Category::Strings)
|
.category(Category::Experimental)
|
||||||
.input_output_types(vec![(Type::Nothing, Type::String)])
|
.input_output_types(vec![(Type::Nothing, Type::String)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
crates/nu-command/src/experimental/job_flush.rs
Normal file
58
crates/nu-command/src/experimental/job_flush.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JobFlush;
|
||||||
|
|
||||||
|
impl Command for JobFlush {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"job flush"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Clear this job's mailbox."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"
|
||||||
|
This command removes all messages in the mailbox of the current job.
|
||||||
|
If a message is received while this command is executing, it may also be discarded.
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("job flush")
|
||||||
|
.category(Category::Experimental)
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||||
|
.allow_variants_without_examples(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let mut mailbox = engine_state
|
||||||
|
.current_job
|
||||||
|
.mailbox
|
||||||
|
.lock()
|
||||||
|
.expect("failed to acquire lock");
|
||||||
|
|
||||||
|
mailbox.clear();
|
||||||
|
|
||||||
|
Ok(Value::nothing(call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![Example {
|
||||||
|
example: "job flush",
|
||||||
|
description: "Clear the mailbox of the current job.",
|
||||||
|
result: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
50
crates/nu-command/src/experimental/job_id.rs
Normal file
50
crates/nu-command/src/experimental/job_id.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JobId;
|
||||||
|
|
||||||
|
impl Command for JobId {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"job id"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Get id of current job."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
"This command returns the job id for the current background job.
|
||||||
|
The special id 0 indicates that this command was not called from a background job thread, and
|
||||||
|
was instead spawned by main nushell execution thread."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("job id")
|
||||||
|
.category(Category::Experimental)
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::Int)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["self", "this", "my-id", "this-id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
|
|
||||||
|
Ok(Value::int(engine_state.current_job.id.get() as i64, head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![Example {
|
||||||
|
example: "job id",
|
||||||
|
description: "Get id of current job",
|
||||||
|
result: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ impl Command for JobList {
|
|||||||
let values = jobs
|
let values = jobs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, job)| {
|
.map(|(id, job)| {
|
||||||
let record = record! {
|
let mut record = record! {
|
||||||
"id" => Value::int(id.get() as i64, head),
|
"id" => Value::int(id.get() as i64, head),
|
||||||
"type" => match job {
|
"type" => match job {
|
||||||
Job::Thread(_) => Value::string("thread", head),
|
Job::Thread(_) => Value::string("thread", head),
|
||||||
@ -52,12 +52,16 @@ impl Command for JobList {
|
|||||||
head,
|
head,
|
||||||
),
|
),
|
||||||
|
|
||||||
Job::Frozen(FrozenJob { unfreeze }) => {
|
Job::Frozen(FrozenJob { unfreeze, .. }) => {
|
||||||
Value::list(vec![ Value::int(unfreeze.pid() as i64, head) ], head)
|
Value::list(vec![ Value::int(unfreeze.pid() as i64, head) ], head)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(tag) = job.tag() {
|
||||||
|
record.push("tag", Value::string(tag, head));
|
||||||
|
}
|
||||||
|
|
||||||
Value::record(record, head)
|
Value::record(record, head)
|
||||||
})
|
})
|
||||||
.collect::<Vec<Value>>();
|
.collect::<Vec<Value>>();
|
||||||
|
181
crates/nu-command/src/experimental/job_recv.rs
Normal file
181
crates/nu-command/src/experimental/job_recv.rs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
use std::{
|
||||||
|
sync::mpsc::{RecvTimeoutError, TryRecvError},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
use nu_protocol::{
|
||||||
|
engine::{FilterTag, Mailbox},
|
||||||
|
Signals,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JobRecv;
|
||||||
|
|
||||||
|
const CTRL_C_CHECK_INTERVAL: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
|
impl Command for JobRecv {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"job recv"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Read a message from the mailbox."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"When messages are sent to the current process, they get stored in what is called the "mailbox".
|
||||||
|
This commands reads and returns a message from the mailbox, in a first-in-first-out fashion.
|
||||||
|
j
|
||||||
|
Messages may have numeric flags attached to them. This commands supports filtering out messages that do not satisfy a given tag, by using the `tag` flag.
|
||||||
|
If no tag is specified, this command will accept any message.
|
||||||
|
|
||||||
|
If no message with the specified tag (if any) is available in the mailbox, this command will block the current thread until one arrives.
|
||||||
|
By default this command block indefinitely until a matching message arrives, but a timeout duration can be specified.
|
||||||
|
If a timeout duration of zero is specified, it will succeed only if there already is a message in the mailbox.
|
||||||
|
|
||||||
|
Note: When using par-each, only one thread at a time can utilize this command.
|
||||||
|
In the case of two or more threads running this command, they will wait until other threads are done using it,
|
||||||
|
in no particular order, regardless of the specified timeout parameter.
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("job recv")
|
||||||
|
.category(Category::Experimental)
|
||||||
|
.named("tag", SyntaxShape::Int, "A tag for the message", None)
|
||||||
|
.named(
|
||||||
|
"timeout",
|
||||||
|
SyntaxShape::Duration,
|
||||||
|
"The maximum time duration to wait for.",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||||
|
.allow_variants_without_examples(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["receive"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
|
|
||||||
|
let tag_arg: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "tag")?;
|
||||||
|
|
||||||
|
if let Some(tag) = tag_arg {
|
||||||
|
if tag.item < 0 {
|
||||||
|
return Err(ShellError::NeedsPositiveValue { span: tag.span });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tag = tag_arg.map(|it| it.item as FilterTag);
|
||||||
|
|
||||||
|
let duration: Option<i64> = call.get_flag(engine_state, stack, "timeout")?;
|
||||||
|
|
||||||
|
let timeout = duration.map(|it| Duration::from_nanos(it as u64));
|
||||||
|
|
||||||
|
let mut mailbox = engine_state
|
||||||
|
.current_job
|
||||||
|
.mailbox
|
||||||
|
.lock()
|
||||||
|
.expect("failed to acquire lock");
|
||||||
|
|
||||||
|
if let Some(timeout) = timeout {
|
||||||
|
if timeout == Duration::ZERO {
|
||||||
|
recv_instantly(&mut mailbox, tag, head)
|
||||||
|
} else {
|
||||||
|
recv_with_time_limit(&mut mailbox, tag, engine_state.signals(), head, timeout)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recv_without_time_limit(&mut mailbox, tag, engine_state.signals(), head)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
example: "job recv",
|
||||||
|
description: "Block the current thread while no message arrives",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "job recv --timeout 10sec",
|
||||||
|
description: "Receive a message, wait for at most 10 seconds.",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "job recv --timeout 0sec",
|
||||||
|
description: "Get a message or fail if no message is available immediately",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recv_without_time_limit(
|
||||||
|
mailbox: &mut Mailbox,
|
||||||
|
tag: Option<FilterTag>,
|
||||||
|
signals: &Signals,
|
||||||
|
span: Span,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
loop {
|
||||||
|
if signals.interrupted() {
|
||||||
|
return Err(ShellError::Interrupted { span });
|
||||||
|
}
|
||||||
|
match mailbox.recv_timeout(tag, CTRL_C_CHECK_INTERVAL) {
|
||||||
|
Ok(value) => return Ok(value),
|
||||||
|
Err(RecvTimeoutError::Timeout) => {} // try again
|
||||||
|
Err(RecvTimeoutError::Disconnected) => return Err(ShellError::Interrupted { span }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recv_instantly(
|
||||||
|
mailbox: &mut Mailbox,
|
||||||
|
tag: Option<FilterTag>,
|
||||||
|
span: Span,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
match mailbox.try_recv(tag) {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(TryRecvError::Empty) => Err(ShellError::RecvTimeout { span }),
|
||||||
|
Err(TryRecvError::Disconnected) => Err(ShellError::Interrupted { span }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recv_with_time_limit(
|
||||||
|
mailbox: &mut Mailbox,
|
||||||
|
tag: Option<FilterTag>,
|
||||||
|
signals: &Signals,
|
||||||
|
span: Span,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let deadline = Instant::now() + timeout;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if signals.interrupted() {
|
||||||
|
return Err(ShellError::Interrupted { span });
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_until_deadline = deadline.saturating_duration_since(Instant::now());
|
||||||
|
|
||||||
|
let time_to_sleep = time_until_deadline.min(CTRL_C_CHECK_INTERVAL);
|
||||||
|
|
||||||
|
match mailbox.recv_timeout(tag, time_to_sleep) {
|
||||||
|
Ok(value) => return Ok(value),
|
||||||
|
Err(RecvTimeoutError::Timeout) => {} // try again
|
||||||
|
Err(RecvTimeoutError::Disconnected) => return Err(ShellError::Interrupted { span }),
|
||||||
|
}
|
||||||
|
|
||||||
|
if time_until_deadline.is_zero() {
|
||||||
|
return Err(ShellError::RecvTimeout { span });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
crates/nu-command/src/experimental/job_send.rs
Normal file
112
crates/nu-command/src/experimental/job_send.rs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{engine::FilterTag, JobId};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JobSend;
|
||||||
|
|
||||||
|
impl Command for JobSend {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"job send"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Send a message to the mailbox of a job."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"
|
||||||
|
This command sends a message to a background job, which can then read sent messages
|
||||||
|
in a first-in-first-out fashion with `job recv`. When it does so, it may additionally specify a numeric filter tag,
|
||||||
|
in which case it will only read messages sent with the exact same filter tag.
|
||||||
|
In particular, the id 0 refers to the main/initial nushell thread.
|
||||||
|
|
||||||
|
A message can be any nushell value, and streams are always collected before being sent.
|
||||||
|
|
||||||
|
This command never blocks.
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("job send")
|
||||||
|
.category(Category::Experimental)
|
||||||
|
.required(
|
||||||
|
"id",
|
||||||
|
SyntaxShape::Int,
|
||||||
|
"The id of the job to send the message to.",
|
||||||
|
)
|
||||||
|
.named("tag", SyntaxShape::Int, "A tag for the message", None)
|
||||||
|
.input_output_types(vec![(Type::Any, Type::Nothing)])
|
||||||
|
.allow_variants_without_examples(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
|
|
||||||
|
let id_arg: Spanned<i64> = call.req(engine_state, stack, 0)?;
|
||||||
|
let tag_arg: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "tag")?;
|
||||||
|
|
||||||
|
let id = id_arg.item;
|
||||||
|
|
||||||
|
if id < 0 {
|
||||||
|
return Err(ShellError::NeedsPositiveValue { span: id_arg.span });
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tag) = tag_arg {
|
||||||
|
if tag.item < 0 {
|
||||||
|
return Err(ShellError::NeedsPositiveValue { span: tag.span });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tag = tag_arg.map(|it| it.item as FilterTag);
|
||||||
|
|
||||||
|
if id == 0 {
|
||||||
|
engine_state
|
||||||
|
.root_job_sender
|
||||||
|
.send((tag, input))
|
||||||
|
.expect("this should NEVER happen.");
|
||||||
|
} else {
|
||||||
|
let jobs = engine_state.jobs.lock().expect("failed to acquire lock");
|
||||||
|
|
||||||
|
if let Some(job) = jobs.lookup(JobId::new(id as usize)) {
|
||||||
|
match job {
|
||||||
|
nu_protocol::engine::Job::Thread(thread_job) => {
|
||||||
|
// it is ok to send this value while holding the lock, because
|
||||||
|
// mail channels are always unbounded, so this send never blocks
|
||||||
|
let _ = thread_job.sender.send((tag, input));
|
||||||
|
}
|
||||||
|
nu_protocol::engine::Job::Frozen(_) => {
|
||||||
|
return Err(ShellError::JobIsFrozen {
|
||||||
|
id: id as usize,
|
||||||
|
span: id_arg.span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(ShellError::JobNotFound {
|
||||||
|
id: id as usize,
|
||||||
|
span: id_arg.span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::nothing(head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![Example {
|
||||||
|
example: "let id = job spawn { job recv | save sent.txt }; 'hi' | job send $id",
|
||||||
|
description: "Send a message to a newly spawned job",
|
||||||
|
result: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,15 @@
|
|||||||
use std::{
|
use std::{
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, AtomicU32},
|
atomic::{AtomicBool, AtomicU32},
|
||||||
Arc,
|
mpsc, Arc, Mutex,
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use nu_engine::{command_prelude::*, ClosureEvalOnce};
|
use nu_engine::{command_prelude::*, ClosureEvalOnce};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{Closure, Job, ThreadJob},
|
engine::{Closure, CurrentJob, Job, Mailbox, Redirection, ThreadJob},
|
||||||
report_shell_error, Signals,
|
report_shell_error, OutDest, Signals,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -28,6 +28,12 @@ impl Command for JobSpawn {
|
|||||||
Signature::build("job spawn")
|
Signature::build("job spawn")
|
||||||
.category(Category::Experimental)
|
.category(Category::Experimental)
|
||||||
.input_output_types(vec![(Type::Nothing, Type::Int)])
|
.input_output_types(vec![(Type::Nothing, Type::Int)])
|
||||||
|
.named(
|
||||||
|
"tag",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"An optional description tag for this job",
|
||||||
|
Some('t'),
|
||||||
|
)
|
||||||
.required(
|
.required(
|
||||||
"closure",
|
"closure",
|
||||||
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
|
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
|
||||||
@ -50,11 +56,12 @@ impl Command for JobSpawn {
|
|||||||
|
|
||||||
let closure: Closure = call.req(engine_state, stack, 0)?;
|
let closure: Closure = call.req(engine_state, stack, 0)?;
|
||||||
|
|
||||||
|
let tag: Option<String> = call.get_flag(engine_state, stack, "tag")?;
|
||||||
|
let job_stack = stack.clone();
|
||||||
|
|
||||||
let mut job_state = engine_state.clone();
|
let mut job_state = engine_state.clone();
|
||||||
job_state.is_interactive = false;
|
job_state.is_interactive = false;
|
||||||
|
|
||||||
let job_stack = stack.clone();
|
|
||||||
|
|
||||||
// the new job should have its ctrl-c independent of foreground
|
// the new job should have its ctrl-c independent of foreground
|
||||||
let job_signals = Signals::new(Arc::new(AtomicBool::new(false)));
|
let job_signals = Signals::new(Arc::new(AtomicBool::new(false)));
|
||||||
job_state.set_signals(job_signals.clone());
|
job_state.set_signals(job_signals.clone());
|
||||||
@ -67,24 +74,37 @@ impl Command for JobSpawn {
|
|||||||
let jobs = job_state.jobs.clone();
|
let jobs = job_state.jobs.clone();
|
||||||
let mut jobs = jobs.lock().expect("jobs lock is poisoned!");
|
let mut jobs = jobs.lock().expect("jobs lock is poisoned!");
|
||||||
|
|
||||||
|
let (send, recv) = mpsc::channel();
|
||||||
|
|
||||||
let id = {
|
let id = {
|
||||||
let thread_job = ThreadJob::new(job_signals);
|
let thread_job = ThreadJob::new(job_signals, tag, send);
|
||||||
job_state.current_thread_job = Some(thread_job.clone());
|
|
||||||
jobs.add_job(Job::Thread(thread_job))
|
let id = jobs.add_job(Job::Thread(thread_job.clone()));
|
||||||
|
|
||||||
|
job_state.current_job = CurrentJob {
|
||||||
|
id,
|
||||||
|
background_thread_job: Some(thread_job),
|
||||||
|
mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
|
||||||
|
};
|
||||||
|
|
||||||
|
id
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = thread::Builder::new()
|
let result = thread::Builder::new()
|
||||||
.name(format!("background job {}", id.get()))
|
.name(format!("background job {}", id.get()))
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
ClosureEvalOnce::new(&job_state, &job_stack, closure)
|
let mut stack = job_stack.reset_pipes();
|
||||||
|
let stack = stack.push_redirection(
|
||||||
|
Some(Redirection::Pipe(OutDest::Null)),
|
||||||
|
Some(Redirection::Pipe(OutDest::Null)),
|
||||||
|
);
|
||||||
|
ClosureEvalOnce::new_preserve_out_dest(&job_state, &stack, closure)
|
||||||
.run_with_input(Value::nothing(head).into_pipeline_data())
|
.run_with_input(Value::nothing(head).into_pipeline_data())
|
||||||
.and_then(|data| data.into_value(head))
|
.and_then(|data| data.drain())
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
if !job_state.signals().interrupted() {
|
if !job_state.signals().interrupted() {
|
||||||
report_shell_error(&job_state, &err);
|
report_shell_error(&job_state, &err);
|
||||||
}
|
}
|
||||||
|
|
||||||
Value::nothing(head)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
|
81
crates/nu-command/src/experimental/job_tag.rs
Normal file
81
crates/nu-command/src/experimental/job_tag.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::JobId;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JobTag;
|
||||||
|
|
||||||
|
impl Command for JobTag {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"job tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Add a description tag to a background job."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("job tag")
|
||||||
|
.category(Category::Experimental)
|
||||||
|
.required("id", SyntaxShape::Int, "The id of the job to tag.")
|
||||||
|
.required(
|
||||||
|
"tag",
|
||||||
|
SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]),
|
||||||
|
"The tag to assign to the job.",
|
||||||
|
)
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["describe", "desc"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
|
|
||||||
|
let id_arg: Spanned<i64> = call.req(engine_state, stack, 0)?;
|
||||||
|
|
||||||
|
if id_arg.item < 0 {
|
||||||
|
return Err(ShellError::NeedsPositiveValue { span: id_arg.span });
|
||||||
|
}
|
||||||
|
|
||||||
|
let id: JobId = JobId::new(id_arg.item as usize);
|
||||||
|
|
||||||
|
let tag: Option<String> = call.req(engine_state, stack, 1)?;
|
||||||
|
|
||||||
|
let mut jobs = engine_state.jobs.lock().expect("jobs lock is poisoned!");
|
||||||
|
|
||||||
|
match jobs.lookup_mut(id) {
|
||||||
|
None => {
|
||||||
|
return Err(ShellError::JobNotFound {
|
||||||
|
id: id.get(),
|
||||||
|
span: head,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(job) => job.assign_tag(tag),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::nothing(head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
example: "let id = job spawn { sleep 10sec }; job tag $id abc ",
|
||||||
|
description: "Tag a newly spawned job",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "let id = job spawn { sleep 10sec }; job tag $id abc; job tag $id null",
|
||||||
|
description: "Remove the tag of a job",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -112,10 +112,13 @@ fn unfreeze_job(
|
|||||||
span,
|
span,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Job::Frozen(FrozenJob { unfreeze: handle }) => {
|
Job::Frozen(FrozenJob {
|
||||||
|
unfreeze: handle,
|
||||||
|
tag,
|
||||||
|
}) => {
|
||||||
let pid = handle.pid();
|
let pid = handle.pid();
|
||||||
|
|
||||||
if let Some(thread_job) = &state.current_thread_job {
|
if let Some(thread_job) = &state.current_thread_job() {
|
||||||
if !thread_job.try_add_pid(pid) {
|
if !thread_job.try_add_pid(pid) {
|
||||||
kill_by_pid(pid.into()).map_err(|err| {
|
kill_by_pid(pid.into()).map_err(|err| {
|
||||||
ShellError::Io(IoError::new_internal(
|
ShellError::Io(IoError::new_internal(
|
||||||
@ -133,7 +136,7 @@ fn unfreeze_job(
|
|||||||
.then(|| state.pipeline_externals_state.clone()),
|
.then(|| state.pipeline_externals_state.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(thread_job) = &state.current_thread_job {
|
if let Some(thread_job) = &state.current_thread_job() {
|
||||||
thread_job.remove_pid(pid);
|
thread_job.remove_pid(pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,8 +144,14 @@ fn unfreeze_job(
|
|||||||
Ok(ForegroundWaitStatus::Frozen(handle)) => {
|
Ok(ForegroundWaitStatus::Frozen(handle)) => {
|
||||||
let mut jobs = state.jobs.lock().expect("jobs lock is poisoned!");
|
let mut jobs = state.jobs.lock().expect("jobs lock is poisoned!");
|
||||||
|
|
||||||
jobs.add_job_with_id(old_id, Job::Frozen(FrozenJob { unfreeze: handle }))
|
jobs.add_job_with_id(
|
||||||
.expect("job was supposed to be removed");
|
old_id,
|
||||||
|
Job::Frozen(FrozenJob {
|
||||||
|
unfreeze: handle,
|
||||||
|
tag,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("job was supposed to be removed");
|
||||||
|
|
||||||
if state.is_interactive {
|
if state.is_interactive {
|
||||||
println!("\nJob {} is re-frozen", old_id.get());
|
println!("\nJob {} is re-frozen", old_id.get());
|
||||||
|
@ -1,18 +1,35 @@
|
|||||||
mod is_admin;
|
mod is_admin;
|
||||||
mod job;
|
mod job;
|
||||||
|
mod job_id;
|
||||||
mod job_kill;
|
mod job_kill;
|
||||||
mod job_list;
|
mod job_list;
|
||||||
mod job_spawn;
|
mod job_spawn;
|
||||||
|
mod job_tag;
|
||||||
|
|
||||||
#[cfg(all(unix, feature = "os"))]
|
#[cfg(all(unix, feature = "os"))]
|
||||||
mod job_unfreeze;
|
mod job_unfreeze;
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
mod job_flush;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
mod job_recv;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
mod job_send;
|
||||||
|
|
||||||
pub use is_admin::IsAdmin;
|
pub use is_admin::IsAdmin;
|
||||||
pub use job::Job;
|
pub use job::Job;
|
||||||
|
pub use job_id::JobId;
|
||||||
pub use job_kill::JobKill;
|
pub use job_kill::JobKill;
|
||||||
pub use job_list::JobList;
|
pub use job_list::JobList;
|
||||||
|
|
||||||
pub use job_spawn::JobSpawn;
|
pub use job_spawn::JobSpawn;
|
||||||
|
pub use job_tag::JobTag;
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use job_flush::JobFlush;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use job_recv::JobRecv;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use job_send::JobSend;
|
||||||
|
|
||||||
#[cfg(all(unix, feature = "os"))]
|
#[cfg(all(unix, feature = "os"))]
|
||||||
pub use job_unfreeze::JobUnfreeze;
|
pub use job_unfreeze::JobUnfreeze;
|
||||||
|
@ -132,7 +132,7 @@ impl Command for Cd {
|
|||||||
stack.set_cwd(path)?;
|
stack.set_cwd(path)?;
|
||||||
Ok(PipelineData::empty())
|
Ok(PipelineData::empty())
|
||||||
}
|
}
|
||||||
PermissionResult::PermissionDenied(_) => {
|
PermissionResult::PermissionDenied => {
|
||||||
Err(IoError::new(std::io::ErrorKind::PermissionDenied, call.head, path).into())
|
Err(IoError::new(std::io::ErrorKind::PermissionDenied, call.head, path).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,11 @@ impl Command for Glob {
|
|||||||
"Whether to filter out symlinks from the returned paths",
|
"Whether to filter out symlinks from the returned paths",
|
||||||
Some('S'),
|
Some('S'),
|
||||||
)
|
)
|
||||||
|
.switch(
|
||||||
|
"follow-symlinks",
|
||||||
|
"Whether to follow symbolic links to their targets",
|
||||||
|
Some('l'),
|
||||||
|
)
|
||||||
.named(
|
.named(
|
||||||
"exclude",
|
"exclude",
|
||||||
SyntaxShape::List(Box::new(SyntaxShape::String)),
|
SyntaxShape::List(Box::new(SyntaxShape::String)),
|
||||||
@ -111,6 +116,11 @@ impl Command for Glob {
|
|||||||
example: r#"glob **/* --exclude [**/target/** **/.git/** */]"#,
|
example: r#"glob **/* --exclude [**/target/** **/.git/** */]"#,
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Search for files following symbolic links to their targets",
|
||||||
|
example: r#"glob "**/*.txt" --follow-symlinks"#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +142,7 @@ impl Command for Glob {
|
|||||||
let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
|
let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
|
||||||
let no_files = call.has_flag(engine_state, stack, "no-file")?;
|
let no_files = call.has_flag(engine_state, stack, "no-file")?;
|
||||||
let no_symlinks = call.has_flag(engine_state, stack, "no-symlink")?;
|
let no_symlinks = call.has_flag(engine_state, stack, "no-symlink")?;
|
||||||
|
let follow_symlinks = call.has_flag(engine_state, stack, "follow-symlinks")?;
|
||||||
let paths_to_exclude: Option<Value> = call.get_flag(engine_state, stack, "exclude")?;
|
let paths_to_exclude: Option<Value> = call.get_flag(engine_state, stack, "exclude")?;
|
||||||
|
|
||||||
let (not_patterns, not_pattern_span): (Vec<String>, Span) = match paths_to_exclude {
|
let (not_patterns, not_pattern_span): (Vec<String>, Span) = match paths_to_exclude {
|
||||||
@ -213,6 +224,11 @@ impl Command for Glob {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let link_behavior = match follow_symlinks {
|
||||||
|
true => wax::LinkBehavior::ReadTarget,
|
||||||
|
false => wax::LinkBehavior::ReadFile,
|
||||||
|
};
|
||||||
|
|
||||||
let result = if !not_patterns.is_empty() {
|
let result = if !not_patterns.is_empty() {
|
||||||
let np: Vec<&str> = not_patterns.iter().map(|s| s as &str).collect();
|
let np: Vec<&str> = not_patterns.iter().map(|s| s as &str).collect();
|
||||||
let glob_results = glob
|
let glob_results = glob
|
||||||
@ -220,7 +236,7 @@ impl Command for Glob {
|
|||||||
path,
|
path,
|
||||||
WalkBehavior {
|
WalkBehavior {
|
||||||
depth: folder_depth,
|
depth: folder_depth,
|
||||||
..Default::default()
|
link: link_behavior,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.into_owned()
|
.into_owned()
|
||||||
@ -247,7 +263,7 @@ impl Command for Glob {
|
|||||||
path,
|
path,
|
||||||
WalkBehavior {
|
WalkBehavior {
|
||||||
depth: folder_depth,
|
depth: folder_depth,
|
||||||
..Default::default()
|
link: link_behavior,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.into_owned()
|
.into_owned()
|
||||||
|
@ -378,10 +378,7 @@ fn ls_for_one_pattern(
|
|||||||
.par_bridge()
|
.par_bridge()
|
||||||
.filter_map(move |x| match x {
|
.filter_map(move |x| match x {
|
||||||
Ok(path) => {
|
Ok(path) => {
|
||||||
let metadata = match std::fs::symlink_metadata(&path) {
|
let metadata = std::fs::symlink_metadata(&path).ok();
|
||||||
Ok(metadata) => Some(metadata),
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
let hidden_dir_clone = Arc::clone(&hidden_dirs);
|
let hidden_dir_clone = Arc::clone(&hidden_dirs);
|
||||||
let mut hidden_dir_mutex = hidden_dir_clone
|
let mut hidden_dir_mutex = hidden_dir_clone
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use nu_engine::{command_prelude::*, current_dir, get_eval_block};
|
use nu_engine::{command_prelude::*, current_dir, eval_call};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast,
|
ast,
|
||||||
|
debugger::{WithDebug, WithoutDebug},
|
||||||
shell_error::{self, io::IoError},
|
shell_error::{self, io::IoError},
|
||||||
DataSource, NuGlob, PipelineMetadata,
|
DataSource, NuGlob, PipelineMetadata,
|
||||||
};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
use crate::database::SQLiteDatabase;
|
use crate::database::SQLiteDatabase;
|
||||||
@ -30,7 +34,14 @@ impl Command for Open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search_terms(&self) -> Vec<&str> {
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
vec!["load", "read", "load_file", "read_file"]
|
vec![
|
||||||
|
"load",
|
||||||
|
"read",
|
||||||
|
"load_file",
|
||||||
|
"read_file",
|
||||||
|
"cat",
|
||||||
|
"get-content",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature(&self) -> nu_protocol::Signature {
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
@ -63,7 +74,6 @@ impl Command for Open {
|
|||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
|
let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
|
||||||
let eval_block = get_eval_block(engine_state);
|
|
||||||
|
|
||||||
if paths.is_empty() && !call.has_positional_args(stack, 0) {
|
if paths.is_empty() && !call.has_positional_args(stack, 0) {
|
||||||
// try to use path from pipeline input if there were no positional or spread args
|
// try to use path from pipeline input if there were no positional or spread args
|
||||||
@ -192,13 +202,16 @@ impl Command for Open {
|
|||||||
|
|
||||||
match converter {
|
match converter {
|
||||||
Some((converter_id, ext)) => {
|
Some((converter_id, ext)) => {
|
||||||
let decl = engine_state.get_decl(converter_id);
|
let open_call = ast::Call {
|
||||||
let command_output = if let Some(block_id) = decl.block_id() {
|
decl_id: converter_id,
|
||||||
let block = engine_state.get_block(block_id);
|
head: call_span,
|
||||||
eval_block(engine_state, stack, block, stream)
|
arguments: vec![],
|
||||||
|
parser_info: HashMap::new(),
|
||||||
|
};
|
||||||
|
let command_output = if engine_state.is_debugging() {
|
||||||
|
eval_call::<WithDebug>(engine_state, stack, &open_call, stream)
|
||||||
} else {
|
} else {
|
||||||
let call = ast::Call::new(call_span);
|
eval_call::<WithoutDebug>(engine_state, stack, &open_call, stream)
|
||||||
decl.run(engine_state, stack, &(&call).into(), stream)
|
|
||||||
};
|
};
|
||||||
output.push(command_output.map_err(|inner| {
|
output.push(command_output.map_err(|inner| {
|
||||||
ShellError::GenericError{
|
ShellError::GenericError{
|
||||||
|
@ -10,11 +10,7 @@ use nu_protocol::{
|
|||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::prelude::FileTypeExt;
|
use std::os::unix::prelude::FileTypeExt;
|
||||||
use std::{
|
use std::{collections::HashMap, io::Error, path::PathBuf};
|
||||||
collections::HashMap,
|
|
||||||
io::{Error, ErrorKind},
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
|
|
||||||
const TRASH_SUPPORTED: bool = cfg!(all(
|
const TRASH_SUPPORTED: bool = cfg!(all(
|
||||||
feature = "trash-support",
|
feature = "trash-support",
|
||||||
@ -379,7 +375,7 @@ fn rm(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let result = if let Err(e) = interaction {
|
let result = if let Err(e) = interaction {
|
||||||
Err(Error::new(ErrorKind::Other, &*e.to_string()))
|
Err(Error::other(&*e.to_string()))
|
||||||
} else if interactive && !confirmed {
|
} else if interactive && !confirmed {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
|
} else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
|
||||||
@ -389,7 +385,7 @@ fn rm(
|
|||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
trash::delete(&f).map_err(|e: trash::Error| {
|
trash::delete(&f).map_err(|e: trash::Error| {
|
||||||
Error::new(ErrorKind::Other, format!("{e:?}\nTry '--permanent' flag"))
|
Error::other(format!("{e:?}\nTry '--permanent' flag"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,17 +158,15 @@ impl Command for UTouch {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut expanded_globs = glob(
|
let mut expanded_globs =
|
||||||
&file_path.to_string_lossy(),
|
glob(&file_path.to_string_lossy(), engine_state.signals().clone())
|
||||||
Some(engine_state.signals().clone()),
|
.unwrap_or_else(|_| {
|
||||||
)
|
panic!(
|
||||||
.unwrap_or_else(|_| {
|
"Failed to process file path: {}",
|
||||||
panic!(
|
&file_path.to_string_lossy()
|
||||||
"Failed to process file path: {}",
|
)
|
||||||
&file_path.to_string_lossy()
|
})
|
||||||
)
|
.peekable();
|
||||||
})
|
|
||||||
.peekable();
|
|
||||||
|
|
||||||
if expanded_globs.peek().is_none() {
|
if expanded_globs.peek().is_none() {
|
||||||
let file_name = file_path.file_name().unwrap_or_else(|| {
|
let file_name = file_path.file_name().unwrap_or_else(|| {
|
||||||
|
@ -30,6 +30,11 @@ impl Command for All {
|
|||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
|
Example {
|
||||||
|
description: "Check if a list contains only true values",
|
||||||
|
example: "[false true true false] | all {}",
|
||||||
|
result: Some(Value::test_bool(false)),
|
||||||
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Check if each row's status is the string 'UP'",
|
description: "Check if each row's status is the string 'UP'",
|
||||||
example: "[[status]; [UP] [UP]] | all {|el| $el.status == UP }",
|
example: "[[status]; [UP] [UP]] | all {|el| $el.status == UP }",
|
||||||
|
@ -30,6 +30,11 @@ impl Command for Any {
|
|||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![
|
vec![
|
||||||
|
Example {
|
||||||
|
description: "Check if a list contains any true values",
|
||||||
|
example: "[false true true false] | any {}",
|
||||||
|
result: Some(Value::test_bool(true)),
|
||||||
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Check if any row's status is the string 'DOWN'",
|
description: "Check if any row's status is the string 'DOWN'",
|
||||||
example: "[[status]; [UP] [DOWN] [UP]] | any {|el| $el.status == DOWN }",
|
example: "[[status]; [UP] [DOWN] [UP]] | any {|el| $el.status == DOWN }",
|
||||||
|
@ -243,7 +243,7 @@ mod test {
|
|||||||
let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>();
|
let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
chunks,
|
chunks,
|
||||||
[s[..4].as_bytes(), s[4..8].as_bytes(), s[8..].as_bytes()]
|
[&s.as_bytes()[..4], &s.as_bytes()[4..8], &s.as_bytes()[8..]]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ mod test {
|
|||||||
let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>();
|
let chunks = chunk_read.map(|e| e.unwrap()).collect::<Vec<_>>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
chunks,
|
chunks,
|
||||||
[s[..4].as_bytes(), s[4..8].as_bytes(), s[8..].as_bytes()]
|
[&s.as_bytes()[..4], &s.as_bytes()[4..8], &s.as_bytes()[8..]]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{ListStream, Signals};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Default;
|
pub struct Default;
|
||||||
@ -146,6 +147,20 @@ fn default(
|
|||||||
&& matches!(input, PipelineData::Value(ref value, _) if value.is_empty()))
|
&& matches!(input, PipelineData::Value(ref value, _) if value.is_empty()))
|
||||||
{
|
{
|
||||||
Ok(value.into_pipeline_data())
|
Ok(value.into_pipeline_data())
|
||||||
|
} else if default_when_empty && matches!(input, PipelineData::ListStream(..)) {
|
||||||
|
let PipelineData::ListStream(ls, metadata) = input else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let span = ls.span();
|
||||||
|
let mut stream = ls.into_inner().peekable();
|
||||||
|
if stream.peek().is_none() {
|
||||||
|
return Ok(value.into_pipeline_data());
|
||||||
|
}
|
||||||
|
|
||||||
|
// stream's internal state already preserves the original signals config, so if this
|
||||||
|
// Signals::empty list stream gets interrupted it will be caught by the underlying iterator
|
||||||
|
let ls = ListStream::new(stream, span, Signals::empty());
|
||||||
|
Ok(PipelineData::ListStream(ls, metadata))
|
||||||
} else {
|
} else {
|
||||||
Ok(input)
|
Ok(input)
|
||||||
}
|
}
|
||||||
|
@ -255,6 +255,16 @@ fn join_rows(
|
|||||||
config: &Config,
|
config: &Config,
|
||||||
span: Span,
|
span: Span,
|
||||||
) {
|
) {
|
||||||
|
if !this
|
||||||
|
.iter()
|
||||||
|
.any(|this_record| match this_record.as_record() {
|
||||||
|
Ok(record) => record.contains(this_join_key),
|
||||||
|
Err(_) => false,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// `this` table does not contain the join column; do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
for this_row in this {
|
for this_row in this {
|
||||||
if let Value::Record {
|
if let Value::Record {
|
||||||
val: this_record, ..
|
val: this_record, ..
|
||||||
@ -281,39 +291,40 @@ fn join_rows(
|
|||||||
result.push(Value::record(record, span))
|
result.push(Value::record(record, span))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !matches!(join_type, JoinType::Inner) {
|
continue;
|
||||||
// `other` table did not contain any rows matching
|
|
||||||
// `this` row on the join column; emit a single joined
|
|
||||||
// row with null values for columns not present,
|
|
||||||
let other_record = other_keys
|
|
||||||
.iter()
|
|
||||||
.map(|&key| {
|
|
||||||
let val = if Some(key.as_ref()) == shared_join_key {
|
|
||||||
this_record
|
|
||||||
.get(key)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| Value::nothing(span))
|
|
||||||
} else {
|
|
||||||
Value::nothing(span)
|
|
||||||
};
|
|
||||||
|
|
||||||
(key.clone(), val)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let record = match join_type {
|
|
||||||
JoinType::Inner | JoinType::Right => {
|
|
||||||
merge_records(&other_record, this_record, shared_join_key)
|
|
||||||
}
|
|
||||||
JoinType::Left => {
|
|
||||||
merge_records(this_record, &other_record, shared_join_key)
|
|
||||||
}
|
|
||||||
_ => panic!("not implemented"),
|
|
||||||
};
|
|
||||||
|
|
||||||
result.push(Value::record(record, span))
|
|
||||||
}
|
}
|
||||||
} // else { a row is missing a value for the join column }
|
}
|
||||||
|
if !matches!(join_type, JoinType::Inner) {
|
||||||
|
// Either `this` row is missing a value for the join column or
|
||||||
|
// `other` table did not contain any rows matching
|
||||||
|
// `this` row on the join column; emit a single joined
|
||||||
|
// row with null values for columns not present
|
||||||
|
let other_record = other_keys
|
||||||
|
.iter()
|
||||||
|
.map(|&key| {
|
||||||
|
let val = if Some(key.as_ref()) == shared_join_key {
|
||||||
|
this_record
|
||||||
|
.get(key)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| Value::nothing(span))
|
||||||
|
} else {
|
||||||
|
Value::nothing(span)
|
||||||
|
};
|
||||||
|
|
||||||
|
(key.clone(), val)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let record = match join_type {
|
||||||
|
JoinType::Inner | JoinType::Right => {
|
||||||
|
merge_records(&other_record, this_record, shared_join_key)
|
||||||
|
}
|
||||||
|
JoinType::Left => merge_records(this_record, &other_record, shared_join_key),
|
||||||
|
_ => panic!("not implemented"),
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(Value::record(record, span))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,8 +42,9 @@ pub(crate) fn typecheck_merge(lhs: &Value, rhs: &Value, head: Span) -> Result<()
|
|||||||
match (lhs.get_type(), rhs.get_type()) {
|
match (lhs.get_type(), rhs.get_type()) {
|
||||||
(Type::Record { .. }, Type::Record { .. }) => Ok(()),
|
(Type::Record { .. }, Type::Record { .. }) => Ok(()),
|
||||||
(_, _) if is_list_of_records(lhs) && is_list_of_records(rhs) => Ok(()),
|
(_, _) if is_list_of_records(lhs) && is_list_of_records(rhs) => Ok(()),
|
||||||
_ => Err(ShellError::PipelineMismatch {
|
other => Err(ShellError::OnlySupportsThisInputType {
|
||||||
exp_input_type: "input and argument to be both record or both table".to_string(),
|
exp_input_type: "input and argument to be both record or both table".to_string(),
|
||||||
|
wrong_type: format!("{} and {}", other.0, other.1).to_string(),
|
||||||
dst_span: head,
|
dst_span: head,
|
||||||
src_span: lhs.span(),
|
src_span: lhs.span(),
|
||||||
}),
|
}),
|
||||||
|
@ -174,8 +174,9 @@ impl Command for Move {
|
|||||||
PipelineData::Value(Value::Record { val, .. }, ..) => {
|
PipelineData::Value(Value::Record { val, .. }, ..) => {
|
||||||
Ok(move_record_columns(&val, &columns, &location, head)?.into_pipeline_data())
|
Ok(move_record_columns(&val, &columns, &location, head)?.into_pipeline_data())
|
||||||
}
|
}
|
||||||
_ => Err(ShellError::PipelineMismatch {
|
other => Err(ShellError::OnlySupportsThisInputType {
|
||||||
exp_input_type: "record or table".to_string(),
|
exp_input_type: "record or table".to_string(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
dst_span: head,
|
dst_span: head,
|
||||||
src_span: Span::new(head.start, head.start),
|
src_span: Span::new(head.start, head.start),
|
||||||
}),
|
}),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use rand::{prelude::SliceRandom, thread_rng};
|
use rand::{prelude::SliceRandom, rng};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Shuffle;
|
pub struct Shuffle;
|
||||||
@ -31,7 +31,7 @@ impl Command for Shuffle {
|
|||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let metadata = input.metadata();
|
let metadata = input.metadata();
|
||||||
let mut values = input.into_iter_strict(call.head)?.collect::<Vec<_>>();
|
let mut values = input.into_iter_strict(call.head)?.collect::<Vec<_>>();
|
||||||
values.shuffle(&mut thread_rng());
|
values.shuffle(&mut rng());
|
||||||
let iter = values.into_iter();
|
let iter = values.into_iter();
|
||||||
Ok(iter.into_pipeline_data_with_metadata(
|
Ok(iter.into_pipeline_data_with_metadata(
|
||||||
call.head,
|
call.head,
|
||||||
|
@ -184,9 +184,10 @@ impl Command for Sort {
|
|||||||
dst_span: value.span(),
|
dst_span: value.span(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => {
|
ref other => {
|
||||||
return Err(ShellError::PipelineMismatch {
|
return Err(ShellError::OnlySupportsThisInputType {
|
||||||
exp_input_type: "record or list".to_string(),
|
exp_input_type: "record or list".to_string(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
dst_span: call.head,
|
dst_span: call.head,
|
||||||
src_span: value.span(),
|
src_span: value.span(),
|
||||||
})
|
})
|
||||||
|
@ -394,7 +394,7 @@ impl<R: Read> Read for IoTee<R> {
|
|||||||
if let Some(thread) = self.thread.take() {
|
if let Some(thread) = self.thread.take() {
|
||||||
if thread.is_finished() {
|
if thread.is_finished() {
|
||||||
if let Err(err) = thread.join().unwrap_or_else(|_| Err(panic_error())) {
|
if let Err(err) = thread.join().unwrap_or_else(|_| Err(panic_error())) {
|
||||||
return Err(io::Error::new(io::ErrorKind::Other, err));
|
return Err(io::Error::other(err));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.thread = Some(thread)
|
self.thread = Some(thread)
|
||||||
@ -405,7 +405,7 @@ impl<R: Read> Read for IoTee<R> {
|
|||||||
self.sender = None;
|
self.sender = None;
|
||||||
if let Some(thread) = self.thread.take() {
|
if let Some(thread) = self.thread.take() {
|
||||||
if let Err(err) = thread.join().unwrap_or_else(|_| Err(panic_error())) {
|
if let Err(err) = thread.join().unwrap_or_else(|_| Err(panic_error())) {
|
||||||
return Err(io::Error::new(io::ErrorKind::Other, err));
|
return Err(io::Error::other(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(sender) = self.sender.as_mut() {
|
} else if let Some(sender) = self.sender.as_mut() {
|
||||||
|
@ -36,13 +36,13 @@ impl Command for ToMd {
|
|||||||
Example {
|
Example {
|
||||||
description: "Outputs an MD string representing the contents of this table",
|
description: "Outputs an MD string representing the contents of this table",
|
||||||
example: "[[foo bar]; [1 2]] | to md",
|
example: "[[foo bar]; [1 2]] | to md",
|
||||||
result: Some(Value::test_string("|foo|bar|\n|-|-|\n|1|2|\n")),
|
result: Some(Value::test_string("|foo|bar|\n|-|-|\n|1|2|")),
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
description: "Optionally, output a formatted markdown string",
|
description: "Optionally, output a formatted markdown string",
|
||||||
example: "[[foo bar]; [1 2]] | to md --pretty",
|
example: "[[foo bar]; [1 2]] | to md --pretty",
|
||||||
result: Some(Value::test_string(
|
result: Some(Value::test_string(
|
||||||
"| foo | bar |\n| --- | --- |\n| 1 | 2 |\n",
|
"| foo | bar |\n| --- | --- |\n| 1 | 2 |",
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Example {
|
Example {
|
||||||
@ -57,6 +57,13 @@ impl Command for ToMd {
|
|||||||
example: "[0 1 2] | to md --pretty",
|
example: "[0 1 2] | to md --pretty",
|
||||||
result: Some(Value::test_string("0\n1\n2")),
|
result: Some(Value::test_string("0\n1\n2")),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Separate list into markdown tables",
|
||||||
|
example: "[ {foo: 1, bar: 2} {foo: 3, bar: 4} {foo: 5}] | to md --per-element",
|
||||||
|
result: Some(Value::test_string(
|
||||||
|
"|foo|bar|\n|-|-|\n|1|2|\n|3|4|\n|foo|\n|-|\n|5|",
|
||||||
|
)),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,11 +101,14 @@ fn to_md(
|
|||||||
grouped_input
|
grouped_input
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(move |val| match val {
|
.map(move |val| match val {
|
||||||
Value::List { .. } => table(val.into_pipeline_data(), pretty, config),
|
Value::List { .. } => {
|
||||||
|
format!("{}\n", table(val.into_pipeline_data(), pretty, config))
|
||||||
|
}
|
||||||
other => fragment(other, pretty, config),
|
other => fragment(other, pretty, config),
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(""),
|
.join("")
|
||||||
|
.trim(),
|
||||||
head,
|
head,
|
||||||
)
|
)
|
||||||
.into_pipeline_data_with_metadata(Some(metadata)));
|
.into_pipeline_data_with_metadata(Some(metadata)));
|
||||||
@ -152,7 +162,13 @@ fn collect_headers(headers: &[String]) -> (Vec<String>, Vec<usize>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn table(input: PipelineData, pretty: bool, config: &Config) -> String {
|
fn table(input: PipelineData, pretty: bool, config: &Config) -> String {
|
||||||
let vec_of_values = input.into_iter().collect::<Vec<Value>>();
|
let vec_of_values = input
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|val| match val {
|
||||||
|
Value::List { vals, .. } => vals,
|
||||||
|
other => vec![other],
|
||||||
|
})
|
||||||
|
.collect::<Vec<Value>>();
|
||||||
let mut headers = merge_descriptors(&vec_of_values);
|
let mut headers = merge_descriptors(&vec_of_values);
|
||||||
|
|
||||||
let mut empty_header_index = 0;
|
let mut empty_header_index = 0;
|
||||||
@ -464,6 +480,39 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_row_value() {
|
||||||
|
let value = Value::test_list(vec![
|
||||||
|
Value::test_record(record! {
|
||||||
|
"foo" => Value::test_string("1"),
|
||||||
|
"bar" => Value::test_string("2"),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"foo" => Value::test_string("3"),
|
||||||
|
"bar" => Value::test_string("4"),
|
||||||
|
}),
|
||||||
|
Value::test_record(record! {
|
||||||
|
"foo" => Value::test_string("5"),
|
||||||
|
"bar" => Value::test_string(""),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
table(
|
||||||
|
value.clone().into_pipeline_data(),
|
||||||
|
false,
|
||||||
|
&Config::default()
|
||||||
|
),
|
||||||
|
one(r#"
|
||||||
|
|foo|bar|
|
||||||
|
|-|-|
|
||||||
|
|1|2|
|
||||||
|
|3|4|
|
||||||
|
|5||
|
||||||
|
"#)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_content_type_metadata() {
|
fn test_content_type_metadata() {
|
||||||
let mut engine_state = Box::new(EngineState::new());
|
let mut engine_state = Box::new(EngineState::new());
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use chrono::Datelike;
|
||||||
use chrono_humanize::HumanTime;
|
use chrono_humanize::HumanTime;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::{format_duration, shell_error::io::IoError, ByteStream, PipelineMetadata};
|
use nu_protocol::{format_duration, shell_error::io::IoError, ByteStream, PipelineMetadata};
|
||||||
@ -167,7 +168,17 @@ fn local_into_string(
|
|||||||
Value::Filesize { val, .. } => val.to_string(),
|
Value::Filesize { val, .. } => val.to_string(),
|
||||||
Value::Duration { val, .. } => format_duration(val),
|
Value::Duration { val, .. } => format_duration(val),
|
||||||
Value::Date { val, .. } => {
|
Value::Date { val, .. } => {
|
||||||
format!("{} ({})", val.to_rfc2822(), HumanTime::from(val))
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
{
|
||||||
|
if val.year() >= 0 {
|
||||||
|
val.to_rfc2822()
|
||||||
|
} else {
|
||||||
|
val.to_rfc3339()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
HumanTime::from(val)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Value::Range { val, .. } => val.to_string(),
|
Value::Range { val, .. } => val.to_string(),
|
||||||
Value::String { val, .. } => val,
|
Value::String { val, .. } => val,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::math::utils::ensure_bounded;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -21,6 +22,7 @@ impl Command for MathAbs {
|
|||||||
Type::List(Box::new(Type::Duration)),
|
Type::List(Box::new(Type::Duration)),
|
||||||
Type::List(Box::new(Type::Duration)),
|
Type::List(Box::new(Type::Duration)),
|
||||||
),
|
),
|
||||||
|
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.category(Category::Math)
|
.category(Category::Math)
|
||||||
@ -46,6 +48,16 @@ impl Command for MathAbs {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(move |value| abs_helper(value, head), engine_state.signals())
|
input.map(move |value| abs_helper(value, head), engine_state.signals())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +68,16 @@ impl Command for MathAbs {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(
|
input.map(
|
||||||
move |value| abs_helper(value, head),
|
move |value| abs_helper(value, head),
|
||||||
working_set.permanent().signals(),
|
working_set.permanent().signals(),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::math::utils::ensure_bounded;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -16,6 +17,7 @@ impl Command for MathCeil {
|
|||||||
Type::List(Box::new(Type::Number)),
|
Type::List(Box::new(Type::Number)),
|
||||||
Type::List(Box::new(Type::Int)),
|
Type::List(Box::new(Type::Int)),
|
||||||
),
|
),
|
||||||
|
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.category(Category::Math)
|
.category(Category::Math)
|
||||||
@ -45,6 +47,16 @@ impl Command for MathCeil {
|
|||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
}
|
}
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(move |value| operate(value, head), engine_state.signals())
|
input.map(move |value| operate(value, head), engine_state.signals())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +71,16 @@ impl Command for MathCeil {
|
|||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
}
|
}
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(
|
input.map(
|
||||||
move |value| operate(value, head),
|
move |value| operate(value, head),
|
||||||
working_set.permanent().signals(),
|
working_set.permanent().signals(),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::math::utils::ensure_bounded;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -16,6 +17,7 @@ impl Command for MathFloor {
|
|||||||
Type::List(Box::new(Type::Number)),
|
Type::List(Box::new(Type::Number)),
|
||||||
Type::List(Box::new(Type::Int)),
|
Type::List(Box::new(Type::Int)),
|
||||||
),
|
),
|
||||||
|
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.category(Category::Math)
|
.category(Category::Math)
|
||||||
@ -45,6 +47,16 @@ impl Command for MathFloor {
|
|||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
}
|
}
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(move |value| operate(value, head), engine_state.signals())
|
input.map(move |value| operate(value, head), engine_state.signals())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +71,16 @@ impl Command for MathFloor {
|
|||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
}
|
}
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(
|
input.map(
|
||||||
move |value| operate(value, head),
|
move |value| operate(value, head),
|
||||||
working_set.permanent().signals(),
|
working_set.permanent().signals(),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::math::utils::ensure_bounded;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::Signals;
|
use nu_protocol::Signals;
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ impl Command for MathLog {
|
|||||||
Type::List(Box::new(Type::Number)),
|
Type::List(Box::new(Type::Number)),
|
||||||
Type::List(Box::new(Type::Float)),
|
Type::List(Box::new(Type::Float)),
|
||||||
),
|
),
|
||||||
|
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.category(Category::Math)
|
.category(Category::Math)
|
||||||
@ -46,7 +48,18 @@ impl Command for MathLog {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
let base: Spanned<f64> = call.req(engine_state, stack, 0)?;
|
let base: Spanned<f64> = call.req(engine_state, stack, 0)?;
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
log(base, call.head, input, engine_state.signals())
|
log(base, call.head, input, engine_state.signals())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +69,18 @@ impl Command for MathLog {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
let base: Spanned<f64> = call.req_const(working_set, 0)?;
|
let base: Spanned<f64> = call.req_const(working_set, 0)?;
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
log(base, call.head, input, working_set.permanent().signals())
|
log(base, call.head, input, working_set.permanent().signals())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ impl Command for MathMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mode(values: &[Value], _span: Span, head: Span) -> Result<Value, ShellError> {
|
pub fn mode(values: &[Value], span: Span, head: Span) -> Result<Value, ShellError> {
|
||||||
//In e-q, Value doesn't implement Hash or Eq, so we have to get the values inside
|
//In e-q, Value doesn't implement Hash or Eq, so we have to get the values inside
|
||||||
// But f64 doesn't implement Hash, so we get the binary representation to use as
|
// But f64 doesn't implement Hash, so we get the binary representation to use as
|
||||||
// key in the HashMap
|
// key in the HashMap
|
||||||
@ -130,11 +130,11 @@ pub fn mode(values: &[Value], _span: Span, head: Span) -> Result<Value, ShellErr
|
|||||||
NumberTypes::Filesize,
|
NumberTypes::Filesize,
|
||||||
)),
|
)),
|
||||||
Value::Error { error, .. } => Err(*error.clone()),
|
Value::Error { error, .. } => Err(*error.clone()),
|
||||||
other => Err(ShellError::UnsupportedInput {
|
_ => Err(ShellError::UnsupportedInput {
|
||||||
msg: "Unable to give a result with this input".to_string(),
|
msg: "Unable to give a result with this input".to_string(),
|
||||||
input: "value originates from here".into(),
|
input: "value originates from here".into(),
|
||||||
msg_span: head,
|
msg_span: head,
|
||||||
input_span: other.span(),
|
input_span: span,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<HashableType>, ShellError>>()?;
|
.collect::<Result<Vec<HashableType>, ShellError>>()?;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::math::utils::ensure_bounded;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -16,6 +17,7 @@ impl Command for MathRound {
|
|||||||
Type::List(Box::new(Type::Number)),
|
Type::List(Box::new(Type::Number)),
|
||||||
Type::List(Box::new(Type::Number)),
|
Type::List(Box::new(Type::Number)),
|
||||||
),
|
),
|
||||||
|
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.named(
|
.named(
|
||||||
@ -52,6 +54,16 @@ impl Command for MathRound {
|
|||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
}
|
}
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(
|
input.map(
|
||||||
move |value| operate(value, head, precision_param),
|
move |value| operate(value, head, precision_param),
|
||||||
engine_state.signals(),
|
engine_state.signals(),
|
||||||
@ -70,6 +82,16 @@ impl Command for MathRound {
|
|||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
}
|
}
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(
|
input.map(
|
||||||
move |value| operate(value, head, precision_param),
|
move |value| operate(value, head, precision_param),
|
||||||
working_set.permanent().signals(),
|
working_set.permanent().signals(),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::math::utils::ensure_bounded;
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -16,6 +17,7 @@ impl Command for MathSqrt {
|
|||||||
Type::List(Box::new(Type::Number)),
|
Type::List(Box::new(Type::Number)),
|
||||||
Type::List(Box::new(Type::Float)),
|
Type::List(Box::new(Type::Float)),
|
||||||
),
|
),
|
||||||
|
(Type::Range, Type::List(Box::new(Type::Number))),
|
||||||
])
|
])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.category(Category::Math)
|
.category(Category::Math)
|
||||||
@ -45,6 +47,16 @@ impl Command for MathSqrt {
|
|||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
}
|
}
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(move |value| operate(value, head), engine_state.signals())
|
input.map(move |value| operate(value, head), engine_state.signals())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +71,16 @@ impl Command for MathSqrt {
|
|||||||
if matches!(input, PipelineData::Empty) {
|
if matches!(input, PipelineData::Empty) {
|
||||||
return Err(ShellError::PipelineEmpty { dst_span: head });
|
return Err(ShellError::PipelineEmpty { dst_span: head });
|
||||||
}
|
}
|
||||||
|
if let PipelineData::Value(
|
||||||
|
Value::Range {
|
||||||
|
ref val,
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
..,
|
||||||
|
) = input
|
||||||
|
{
|
||||||
|
ensure_bounded(val.as_ref(), internal_span, head)?;
|
||||||
|
}
|
||||||
input.map(
|
input.map(
|
||||||
move |value| operate(value, head),
|
move |value| operate(value, head),
|
||||||
working_set.permanent().signals(),
|
working_set.permanent().signals(),
|
||||||
|
@ -14,6 +14,7 @@ impl Command for MathStddev {
|
|||||||
Signature::build("math stddev")
|
Signature::build("math stddev")
|
||||||
.input_output_types(vec![
|
.input_output_types(vec![
|
||||||
(Type::List(Box::new(Type::Number)), Type::Number),
|
(Type::List(Box::new(Type::Number)), Type::Number),
|
||||||
|
(Type::Range, Type::Number),
|
||||||
(Type::table(), Type::record()),
|
(Type::table(), Type::record()),
|
||||||
(Type::record(), Type::record()),
|
(Type::record(), Type::record()),
|
||||||
])
|
])
|
||||||
@ -53,6 +54,18 @@ impl Command for MathStddev {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let sample = call.has_flag(engine_state, stack, "sample")?;
|
let sample = call.has_flag(engine_state, stack, "sample")?;
|
||||||
|
let name = call.head;
|
||||||
|
let span = input.span().unwrap_or(name);
|
||||||
|
let input: PipelineData = match input.try_expand_range() {
|
||||||
|
Err(_) => {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "Range must be bounded".to_string(),
|
||||||
|
val_span: span,
|
||||||
|
call_span: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(val) => val,
|
||||||
|
};
|
||||||
run_with_function(call, input, compute_stddev(sample))
|
run_with_function(call, input, compute_stddev(sample))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +76,18 @@ impl Command for MathStddev {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let sample = call.has_flag_const(working_set, "sample")?;
|
let sample = call.has_flag_const(working_set, "sample")?;
|
||||||
|
let name = call.head;
|
||||||
|
let span = input.span().unwrap_or(name);
|
||||||
|
let input: PipelineData = match input.try_expand_range() {
|
||||||
|
Err(_) => {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "Range must be bounded".to_string(),
|
||||||
|
val_span: span,
|
||||||
|
call_span: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(val) => val,
|
||||||
|
};
|
||||||
run_with_function(call, input, compute_stddev(sample))
|
run_with_function(call, input, compute_stddev(sample))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use core::{ops::Bound, slice};
|
use core::slice;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::Call, IntoPipelineData, PipelineData, Range, ShellError, Signals, Span, Value,
|
engine::Call, IntoPipelineData, PipelineData, Range, ShellError, Signals, Span, Value,
|
||||||
@ -93,10 +93,7 @@ pub fn calculate(
|
|||||||
Ok(Value::record(record, span))
|
Ok(Value::record(record, span))
|
||||||
}
|
}
|
||||||
PipelineData::Value(Value::Range { val, .. }, ..) => {
|
PipelineData::Value(Value::Range { val, .. }, ..) => {
|
||||||
match *val {
|
ensure_bounded(val.as_ref(), span, name)?;
|
||||||
Range::IntRange(range) => ensure_bounded(range.end(), span, name)?,
|
|
||||||
Range::FloatRange(range) => ensure_bounded(range.end(), span, name)?,
|
|
||||||
}
|
|
||||||
let new_vals: Result<Vec<Value>, ShellError> = val
|
let new_vals: Result<Vec<Value>, ShellError> = val
|
||||||
.into_range_iter(span, Signals::empty())
|
.into_range_iter(span, Signals::empty())
|
||||||
.map(|val| mf(&[val], span, name))
|
.map(|val| mf(&[val], span, name))
|
||||||
@ -105,7 +102,7 @@ pub fn calculate(
|
|||||||
mf(&new_vals?, span, name)
|
mf(&new_vals?, span, name)
|
||||||
}
|
}
|
||||||
PipelineData::Value(val, ..) => mf(&[val], span, name),
|
PipelineData::Value(val, ..) => mf(&[val], span, name),
|
||||||
PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: name }),
|
PipelineData::Empty => Err(ShellError::PipelineEmpty { dst_span: name }),
|
||||||
val => Err(ShellError::UnsupportedInput {
|
val => Err(ShellError::UnsupportedInput {
|
||||||
msg: "Only ints, floats, lists, records, or ranges are supported".into(),
|
msg: "Only ints, floats, lists, records, or ranges are supported".into(),
|
||||||
input: "value originates from here".into(),
|
input: "value originates from here".into(),
|
||||||
@ -117,17 +114,13 @@ pub fn calculate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_bounded<T>(
|
pub fn ensure_bounded(range: &Range, val_span: Span, call_span: Span) -> Result<(), ShellError> {
|
||||||
bound: Bound<T>,
|
if range.is_bounded() {
|
||||||
val_span: Span,
|
return Ok(());
|
||||||
call_span: Span,
|
|
||||||
) -> Result<(), ShellError> {
|
|
||||||
match bound {
|
|
||||||
Bound::<T>::Unbounded => Err(ShellError::IncorrectValue {
|
|
||||||
msg: "Range must be bounded".to_string(),
|
|
||||||
val_span,
|
|
||||||
call_span,
|
|
||||||
}),
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
}
|
||||||
|
Err(ShellError::IncorrectValue {
|
||||||
|
msg: "Range must be bounded".to_string(),
|
||||||
|
val_span,
|
||||||
|
call_span,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ impl Command for MathVariance {
|
|||||||
Signature::build("math variance")
|
Signature::build("math variance")
|
||||||
.input_output_types(vec![
|
.input_output_types(vec![
|
||||||
(Type::List(Box::new(Type::Number)), Type::Number),
|
(Type::List(Box::new(Type::Number)), Type::Number),
|
||||||
|
(Type::Range, Type::Number),
|
||||||
(Type::table(), Type::record()),
|
(Type::table(), Type::record()),
|
||||||
(Type::record(), Type::record()),
|
(Type::record(), Type::record()),
|
||||||
])
|
])
|
||||||
@ -45,6 +46,18 @@ impl Command for MathVariance {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let sample = call.has_flag(engine_state, stack, "sample")?;
|
let sample = call.has_flag(engine_state, stack, "sample")?;
|
||||||
|
let name = call.head;
|
||||||
|
let span = input.span().unwrap_or(name);
|
||||||
|
let input: PipelineData = match input.try_expand_range() {
|
||||||
|
Err(_) => {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "Range must be bounded".to_string(),
|
||||||
|
val_span: span,
|
||||||
|
call_span: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(val) => val,
|
||||||
|
};
|
||||||
run_with_function(call, input, compute_variance(sample))
|
run_with_function(call, input, compute_variance(sample))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +68,18 @@ impl Command for MathVariance {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let sample = call.has_flag_const(working_set, "sample")?;
|
let sample = call.has_flag_const(working_set, "sample")?;
|
||||||
|
let name = call.head;
|
||||||
|
let span = input.span().unwrap_or(name);
|
||||||
|
let input: PipelineData = match input.try_expand_range() {
|
||||||
|
Err(_) => {
|
||||||
|
return Err(ShellError::IncorrectValue {
|
||||||
|
msg: "Range must be bounded".to_string(),
|
||||||
|
val_span: span,
|
||||||
|
call_span: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(val) => val,
|
||||||
|
};
|
||||||
run_with_function(call, input, compute_variance(sample))
|
run_with_function(call, input, compute_variance(sample))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,7 +723,7 @@ fn transform_response_using_content_type(
|
|||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.path_segments()
|
.path_segments()
|
||||||
.and_then(|segments| segments.last())
|
.and_then(|mut segments| segments.next_back())
|
||||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||||
.and_then(|name| {
|
.and_then(|name| {
|
||||||
PathBuf::from(name)
|
PathBuf::from(name)
|
||||||
|
@ -175,7 +175,7 @@ fn run(call: &Call, args: &Arguments, input: PipelineData) -> Result<PipelineDat
|
|||||||
handle_value(stream.into_value(), args, head),
|
handle_value(stream.into_value(), args, head),
|
||||||
metadata,
|
metadata,
|
||||||
)),
|
)),
|
||||||
PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: head }),
|
PipelineData::Empty => Err(ShellError::PipelineEmpty { dst_span: head }),
|
||||||
_ => Err(ShellError::UnsupportedInput {
|
_ => Err(ShellError::UnsupportedInput {
|
||||||
msg: "Input value cannot be joined".to_string(),
|
msg: "Input value cannot be joined".to_string(),
|
||||||
input: "value originates from here".into(),
|
input: "value originates from here".into(),
|
||||||
@ -221,14 +221,21 @@ fn join_list(parts: &[Value], head: Span, span: Span, args: &Arguments) -> Value
|
|||||||
|
|
||||||
Value::list(vals, span)
|
Value::list(vals, span)
|
||||||
}
|
}
|
||||||
Err(_) => Value::error(
|
Err(ShellError::CantConvert { from_type, .. }) => Value::error(
|
||||||
ShellError::PipelineMismatch {
|
ShellError::OnlySupportsThisInputType {
|
||||||
exp_input_type: "string or record".into(),
|
exp_input_type: "string or record".into(),
|
||||||
|
wrong_type: from_type,
|
||||||
dst_span: head,
|
dst_span: head,
|
||||||
src_span: span,
|
src_span: span,
|
||||||
},
|
},
|
||||||
span,
|
span,
|
||||||
),
|
),
|
||||||
|
Err(_) => Value::error(
|
||||||
|
ShellError::NushellFailed {
|
||||||
|
msg: "failed to join path".into(),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,21 +51,11 @@ fn handle_invalid_values(rest: Value, name: Span) -> Value {
|
|||||||
fn err_from_value(rest: &Value, name: Span) -> ShellError {
|
fn err_from_value(rest: &Value, name: Span) -> ShellError {
|
||||||
match rest {
|
match rest {
|
||||||
Value::Error { error, .. } => *error.clone(),
|
Value::Error { error, .. } => *error.clone(),
|
||||||
_ => {
|
_ => ShellError::OnlySupportsThisInputType {
|
||||||
if rest.is_nothing() {
|
exp_input_type: "string, record or list".into(),
|
||||||
ShellError::OnlySupportsThisInputType {
|
wrong_type: rest.get_type().to_string(),
|
||||||
exp_input_type: "string, record or list".into(),
|
dst_span: name,
|
||||||
wrong_type: "nothing".into(),
|
src_span: rest.span(),
|
||||||
dst_span: name,
|
},
|
||||||
src_span: rest.span(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ShellError::PipelineMismatch {
|
|
||||||
exp_input_type: "string, row or list".into(),
|
|
||||||
dst_span: name,
|
|
||||||
src_span: rest.span(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,11 @@ impl Command for Kill {
|
|||||||
let signature = Signature::build("kill")
|
let signature = Signature::build("kill")
|
||||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
.required(
|
.rest(
|
||||||
"pid",
|
"pid",
|
||||||
SyntaxShape::Int,
|
SyntaxShape::Int,
|
||||||
"Process id of process that is to be killed.",
|
"Process ids of processes that are to be killed.",
|
||||||
)
|
)
|
||||||
.rest("rest", SyntaxShape::Int, "Rest of processes to kill.")
|
|
||||||
.switch("force", "forcefully kill the process", Some('f'))
|
.switch("force", "forcefully kill the process", Some('f'))
|
||||||
.switch("quiet", "won't print anything to the console", Some('q'))
|
.switch("quiet", "won't print anything to the console", Some('q'))
|
||||||
.category(Category::Platform);
|
.category(Category::Platform);
|
||||||
@ -51,12 +50,18 @@ impl Command for Kill {
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let pid: i64 = call.req(engine_state, stack, 0)?;
|
let pids: Vec<i64> = call.rest(engine_state, stack, 0)?;
|
||||||
let rest: Vec<i64> = call.rest(engine_state, stack, 1)?;
|
|
||||||
let force: bool = call.has_flag(engine_state, stack, "force")?;
|
let force: bool = call.has_flag(engine_state, stack, "force")?;
|
||||||
let signal: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "signal")?;
|
let signal: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "signal")?;
|
||||||
let quiet: bool = call.has_flag(engine_state, stack, "quiet")?;
|
let quiet: bool = call.has_flag(engine_state, stack, "quiet")?;
|
||||||
|
|
||||||
|
if pids.is_empty() {
|
||||||
|
return Err(ShellError::MissingParameter {
|
||||||
|
param_name: "pid".to_string(),
|
||||||
|
span: call.arguments_span(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if cfg!(unix) {
|
if cfg!(unix) {
|
||||||
if let (
|
if let (
|
||||||
true,
|
true,
|
||||||
@ -83,7 +88,7 @@ impl Command for Kill {
|
|||||||
|
|
||||||
let mut cmd = build_kill_command(
|
let mut cmd = build_kill_command(
|
||||||
force,
|
force,
|
||||||
std::iter::once(pid).chain(rest),
|
pids.iter().copied(),
|
||||||
signal.map(|spanned| spanned.item as u32),
|
signal.map(|spanned| spanned.item as u32),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
use super::byte_stream::{random_byte_stream, RandomDistribution};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use rand::{thread_rng, RngCore};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RandomBinary;
|
pub struct RandomBinary;
|
||||||
@ -57,12 +57,12 @@ impl Command for RandomBinary {
|
|||||||
}),
|
}),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let mut rng = thread_rng();
|
Ok(random_byte_stream(
|
||||||
|
RandomDistribution::Binary,
|
||||||
let mut out = vec![0u8; length];
|
length,
|
||||||
rng.fill_bytes(&mut out);
|
call.head,
|
||||||
|
engine_state.signals().clone(),
|
||||||
Ok(Value::binary(out, call.head).into_pipeline_data())
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
use rand::random_bool;
|
||||||
use rand::prelude::{thread_rng, Rng};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RandomBool;
|
pub struct RandomBool;
|
||||||
@ -77,8 +76,7 @@ fn bool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rng = thread_rng();
|
let bool_result: bool = random_bool(probability);
|
||||||
let bool_result: bool = rng.gen_bool(probability);
|
|
||||||
|
|
||||||
Ok(PipelineData::Value(Value::bool(bool_result, span), None))
|
Ok(PipelineData::Value(Value::bool(bool_result, span), None))
|
||||||
}
|
}
|
||||||
|
49
crates/nu-command/src/random/byte_stream.rs
Normal file
49
crates/nu-command/src/random/byte_stream.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::Signals;
|
||||||
|
use rand::{
|
||||||
|
distr::{Alphanumeric, StandardUniform},
|
||||||
|
rng, Rng,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) enum RandomDistribution {
|
||||||
|
Binary,
|
||||||
|
Alphanumeric,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn random_byte_stream(
|
||||||
|
distribution: RandomDistribution,
|
||||||
|
length: usize,
|
||||||
|
span: Span,
|
||||||
|
signals: Signals,
|
||||||
|
) -> PipelineData {
|
||||||
|
let stream_type = match distribution {
|
||||||
|
RandomDistribution::Binary => ByteStreamType::Binary,
|
||||||
|
RandomDistribution::Alphanumeric => ByteStreamType::String,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OUTPUT_CHUNK_SIZE: usize = 8192;
|
||||||
|
let mut remaining_bytes = length;
|
||||||
|
PipelineData::ByteStream(
|
||||||
|
ByteStream::from_fn(span, signals.clone(), stream_type, move |out| {
|
||||||
|
if remaining_bytes == 0 || signals.interrupted() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes_to_write = std::cmp::min(remaining_bytes, OUTPUT_CHUNK_SIZE);
|
||||||
|
|
||||||
|
let rng = rng();
|
||||||
|
let byte_iter: Box<dyn Iterator<Item = u8>> = match distribution {
|
||||||
|
RandomDistribution::Binary => Box::new(rng.sample_iter(StandardUniform)),
|
||||||
|
RandomDistribution::Alphanumeric => Box::new(rng.sample_iter(Alphanumeric)),
|
||||||
|
};
|
||||||
|
out.extend(byte_iter.take(bytes_to_write));
|
||||||
|
|
||||||
|
remaining_bytes -= bytes_to_write;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
})
|
||||||
|
.with_known_size(Some(length as u64)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.with_span(span)
|
||||||
|
}
|
@ -1,8 +1,5 @@
|
|||||||
|
use super::byte_stream::{random_byte_stream, RandomDistribution};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use rand::{
|
|
||||||
distributions::{Alphanumeric, Distribution},
|
|
||||||
thread_rng,
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_CHARS_LENGTH: usize = 25;
|
const DEFAULT_CHARS_LENGTH: usize = 25;
|
||||||
|
|
||||||
@ -71,7 +68,6 @@ fn chars(
|
|||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
call: &Call,
|
call: &Call,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let span = call.head;
|
|
||||||
let length: Option<Value> = call.get_flag(engine_state, stack, "length")?;
|
let length: Option<Value> = call.get_flag(engine_state, stack, "length")?;
|
||||||
let length = if let Some(length_val) = length {
|
let length = if let Some(length_val) = length {
|
||||||
match length_val {
|
match length_val {
|
||||||
@ -97,17 +93,11 @@ fn chars(
|
|||||||
DEFAULT_CHARS_LENGTH
|
DEFAULT_CHARS_LENGTH
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut rng = thread_rng();
|
Ok(random_byte_stream(
|
||||||
|
RandomDistribution::Alphanumeric,
|
||||||
let random_string = Alphanumeric
|
length,
|
||||||
.sample_iter(&mut rng)
|
call.head,
|
||||||
.take(length)
|
engine_state.signals().clone(),
|
||||||
.map(char::from)
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
Ok(PipelineData::Value(
|
|
||||||
Value::string(random_string, span),
|
|
||||||
None,
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::ListStream;
|
use nu_protocol::ListStream;
|
||||||
use rand::prelude::{thread_rng, Rng};
|
use rand::random_range;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RandomDice;
|
pub struct RandomDice;
|
||||||
@ -73,10 +73,7 @@ fn dice(
|
|||||||
let dice: usize = call.get_flag(engine_state, stack, "dice")?.unwrap_or(1);
|
let dice: usize = call.get_flag(engine_state, stack, "dice")?.unwrap_or(1);
|
||||||
let sides: usize = call.get_flag(engine_state, stack, "sides")?.unwrap_or(6);
|
let sides: usize = call.get_flag(engine_state, stack, "sides")?.unwrap_or(6);
|
||||||
|
|
||||||
let iter = (0..dice).map(move |_| {
|
let iter = (0..dice).map(move |_| Value::int(random_range(1..sides + 1) as i64, span));
|
||||||
let mut thread_rng = thread_rng();
|
|
||||||
Value::int(thread_rng.gen_range(1..sides + 1) as i64, span)
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(ListStream::new(iter, span, engine_state.signals().clone()).into())
|
Ok(ListStream::new(iter, span, engine_state.signals().clone()).into())
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::{FloatRange, Range};
|
use nu_protocol::{FloatRange, Range};
|
||||||
use rand::prelude::{thread_rng, Rng};
|
use rand::random_range;
|
||||||
use std::ops::Bound;
|
use std::ops::Bound;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -71,8 +71,6 @@ fn float(
|
|||||||
let span = call.head;
|
let span = call.head;
|
||||||
let range: Option<Spanned<Range>> = call.opt(engine_state, stack, 0)?;
|
let range: Option<Spanned<Range>> = call.opt(engine_state, stack, 0)?;
|
||||||
|
|
||||||
let mut thread_rng = thread_rng();
|
|
||||||
|
|
||||||
match range {
|
match range {
|
||||||
Some(range) => {
|
Some(range) => {
|
||||||
let range_span = range.span;
|
let range_span = range.span;
|
||||||
@ -90,15 +88,15 @@ fn float(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let value = match range.end() {
|
let value = match range.end() {
|
||||||
Bound::Included(end) => thread_rng.gen_range(range.start()..=end),
|
Bound::Included(end) => random_range(range.start()..=end),
|
||||||
Bound::Excluded(end) => thread_rng.gen_range(range.start()..end),
|
Bound::Excluded(end) => random_range(range.start()..end),
|
||||||
Bound::Unbounded => thread_rng.gen_range(range.start()..f64::INFINITY),
|
Bound::Unbounded => random_range(range.start()..f64::INFINITY),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PipelineData::Value(Value::float(value, span), None))
|
Ok(PipelineData::Value(Value::float(value, span), None))
|
||||||
}
|
}
|
||||||
None => Ok(PipelineData::Value(
|
None => Ok(PipelineData::Value(
|
||||||
Value::float(thread_rng.gen_range(0.0..1.0), span),
|
Value::float(random_range(0.0..1.0), span),
|
||||||
None,
|
None,
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::Range;
|
use nu_protocol::Range;
|
||||||
use rand::prelude::{thread_rng, Rng};
|
use rand::random_range;
|
||||||
use std::ops::Bound;
|
use std::ops::Bound;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -75,8 +75,6 @@ fn integer(
|
|||||||
let span = call.head;
|
let span = call.head;
|
||||||
let range: Option<Spanned<Range>> = call.opt(engine_state, stack, 0)?;
|
let range: Option<Spanned<Range>> = call.opt(engine_state, stack, 0)?;
|
||||||
|
|
||||||
let mut thread_rng = thread_rng();
|
|
||||||
|
|
||||||
match range {
|
match range {
|
||||||
Some(range) => {
|
Some(range) => {
|
||||||
let range_span = range.span;
|
let range_span = range.span;
|
||||||
@ -94,9 +92,9 @@ fn integer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let value = match range.end() {
|
let value = match range.end() {
|
||||||
Bound::Included(end) => thread_rng.gen_range(range.start()..=end),
|
Bound::Included(end) => random_range(range.start()..=end),
|
||||||
Bound::Excluded(end) => thread_rng.gen_range(range.start()..end),
|
Bound::Excluded(end) => random_range(range.start()..end),
|
||||||
Bound::Unbounded => thread_rng.gen_range(range.start()..=i64::MAX),
|
Bound::Unbounded => random_range(range.start()..=i64::MAX),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PipelineData::Value(Value::int(value, span), None))
|
Ok(PipelineData::Value(Value::int(value, span), None))
|
||||||
@ -110,7 +108,7 @@ fn integer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Ok(PipelineData::Value(
|
None => Ok(PipelineData::Value(
|
||||||
Value::int(thread_rng.gen_range(0..=i64::MAX), span),
|
Value::int(random_range(0..=i64::MAX), span),
|
||||||
None,
|
None,
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
mod binary;
|
mod binary;
|
||||||
mod bool;
|
mod bool;
|
||||||
|
mod byte_stream;
|
||||||
mod chars;
|
mod chars;
|
||||||
mod dice;
|
mod dice;
|
||||||
mod float;
|
mod float;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user