Compare commits

..

4 Commits

Author SHA1 Message Date
2a08a18b26 Bump version to 0.92.2 (#12402) 2024-04-10 23:15:51 +02:00
d5aad7a4ef Tweak release workflow after 0.92.1 lessons (#12401)
Encountered repeated build failure that vanished after clearing the
existing build caches.

- Don't `fail-fast` (this is highly annoying, if a severe problem is
detected, there is still the possibility to intervene manually)
- Don't cache the build process, we do it rarely so any potential
speed-up is offset by the uncertainty if this affects the artefact
2024-04-10 23:00:59 +02:00
5bc21fbb0a Bump our Rust version to stable
This was prompted by CVE-2024-24576

- https://nvd.nist.gov/vuln/detail/CVE-2024-24576
- https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html
- https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/

Affected is launching commands on Windows with arbitrary arguments,
which is the case for Nushell's external invocation on Windows

Rust has fixed this quoting vulnerability in 1.77.2 (latest stable at
time of commit)

We will thus use this version for our builds and recommend all our
packaging/distribution maintainers to use this version of Rust when
building Nushell.
2024-04-10 22:53:07 +02:00
f136e0601d Update MSRV following rust-toolchain.toml (#12455)
Also update the `rust-version` in `Cargo.toml` following the update to
`rust-toolchain.toml` in #12258

Added a CI check to verify any future PRs trying to update one will also
have to update the other. (using `std-lib-and-python-virtualenv` job as
this already includes a fresh `nu` binary for a little toml munching
script)
2024-04-10 22:51:03 +02:00
1728 changed files with 68131 additions and 141336 deletions

View File

@ -13,7 +13,7 @@ body:
id: repro id: repro
attributes: attributes:
label: How to reproduce label: How to reproduce
description: Steps to reproduce the behavior (including succinct code examples or screenshots of the observed behavior) description: Steps to reproduce the behavior
placeholder: | placeholder: |
1. 1.
2. 2.
@ -28,6 +28,13 @@ body:
placeholder: I expected nu to... placeholder: I expected nu to...
validations: validations:
required: true required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add any relevant screenshots here, if any
validations:
required: false
- type: textarea - type: textarea
id: config id: config
attributes: attributes:
@ -48,3 +55,10 @@ body:
| installed_plugins | binaryview, chart bar, chart line, fetch, from bson, from sqlite, inc, match, post, ps, query json, s3, selector, start, sys, textview, to bson, to sqlite, tree, xpath | | installed_plugins | binaryview, chart bar, chart line, fetch, from bson, from sqlite, inc, match, post, ps, query json, s3, selector, start, sys, textview, to bson, to sqlite, tree, xpath |
validations: validations:
required: true required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

View File

@ -18,21 +18,6 @@ updates:
ignore: ignore:
- dependency-name: "*" - dependency-name: "*"
update-types: ["version-update:semver-patch"] update-types: ["version-update:semver-patch"]
groups:
# Only update polars as a whole as there are many subcrates that need to
# be updated at once. We explicitly depend on some of them, so batch their
# updates to not take up dependabot PR slots with dysfunctional PRs
polars:
patterns:
- "polars"
- "polars-*"
# uutils/coreutils also versions all their workspace crates the same at the moment
# Most of them have bleeding edge version requirements (some not)
# see: https://github.com/uutils/coreutils/blob/main/Cargo.toml
uutils:
patterns:
- "uucore"
- "uu_*"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:

40
.github/labeler.yml vendored
View File

@ -1,40 +0,0 @@
# 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_*/**

View File

@ -26,7 +26,7 @@ Make sure you've run and fixed any issues with these commands:
- `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library
> **Note** > **Note**
> from `nushell` you can also use the `toolkit` as follows > from `nushell` you can also use the `toolkit` as follows

View File

@ -19,7 +19,7 @@ jobs:
# Prevent sudden announcement of a new advisory from failing ci: # Prevent sudden announcement of a new advisory from failing ci:
continue-on-error: true continue-on-error: true
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.2
- uses: rustsec/audit-check@v2.0.0 - uses: rustsec/audit-check@v1.4.1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,52 +0,0 @@
name: Test on Beta Toolchain
# This workflow is made to run our tests on the beta toolchain to validate that
# the beta toolchain works.
# We do not intend to test here that we are working correctly but rather that
# the beta toolchain works correctly.
# The ci.yml handles our actual testing with our guarantees.
on:
schedule:
# If this workflow fails, GitHub notifications will go to the last person
# who edited this line.
# See: https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/monitoring-workflows/notifications-for-workflow-runs
- cron: '0 0 * * *' # Runs daily at midnight UTC
env:
NUSHELL_CARGO_PROFILE: ci
NU_LOG_LEVEL: DEBUG
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
build-and-test:
# this job is more for testing the beta toolchain and not our tests, so if
# this fails but the tests of the regular ci pass, then this is fine
continue-on-error: true
strategy:
fail-fast: true
matrix:
platform: [windows-latest, macos-latest, ubuntu-22.04]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- run: rustup update beta
- name: Tests
run: cargo +beta test --workspace --profile ci --exclude nu_plugin_*
- name: Check for clean repo
shell: bash
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "there are changes";
git status --porcelain
exit 1
else
echo "no changes in working directory";
fi

View File

@ -3,7 +3,6 @@ on:
push: push:
branches: branches:
- main - main
- 'patch-release-*'
name: continuous-integration name: continuous-integration
@ -11,7 +10,7 @@ env:
NUSHELL_CARGO_PROFILE: ci NUSHELL_CARGO_PROFILE: ci
NU_LOG_LEVEL: DEBUG NU_LOG_LEVEL: DEBUG
# If changing these settings also change toolkit.nu # If changing these settings also change toolkit.nu
CLIPPY_OPTIONS: "-D warnings -D clippy::unwrap_used -D clippy::unchecked_duration_subtraction" CLIPPY_OPTIONS: "-D warnings -D clippy::unwrap_used"
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
@ -22,53 +21,80 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
# Pinning to Ubuntu 22.04 because building on newer Ubuntu versions causes linux-gnu # Pinning to Ubuntu 20.04 because building on newer Ubuntu versions causes linux-gnu
# builds to link against a too-new-for-many-Linux-installs glibc version. Consider # builds to link against a too-new-for-many-Linux-installs glibc version. Consider
# revisiting this when 22.04 is closer to EOL (June 2027) # revisiting this when 20.04 is closer to EOL (April 2025)
# platform: [windows-latest, macos-latest, ubuntu-20.04]
# Using macOS 13 runner because 14 is based on the M1 and has half as much RAM (7 GB, feature: [default, dataframe]
# instead of 14 GB) which is too little for us right now. Revisit when `dfr` commands are include:
# removed and we're only building the `polars` plugin instead - feature: default
platform: [windows-latest, macos-13, ubuntu-22.04] flags: ""
- feature: dataframe
flags: "--features=dataframe"
exclude:
- platform: windows-latest
feature: dataframe
- platform: macos-latest
feature: dataframe
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.2
- 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.8.0
with:
rustflags: ""
- name: cargo fmt - name: cargo fmt
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
# If changing these settings also change toolkit.nu # If changing these settings also change toolkit.nu
- name: Clippy - name: Clippy
run: cargo clippy --workspace --exclude nu_plugin_* -- $CLIPPY_OPTIONS run: cargo clippy --workspace ${{ matrix.flags }} --exclude nu_plugin_* -- $CLIPPY_OPTIONS
# In tests we don't have to deny unwrap # In tests we don't have to deny unwrap
- name: Clippy of tests - name: Clippy of tests
run: cargo clippy --tests --workspace --exclude nu_plugin_* -- -D warnings run: cargo clippy --tests --workspace ${{ matrix.flags }} --exclude nu_plugin_* -- -D warnings
- name: Clippy of benchmarks - name: Clippy of benchmarks
run: cargo clippy --benches --workspace --exclude nu_plugin_* -- -D warnings run: cargo clippy --benches --workspace ${{ matrix.flags }} --exclude nu_plugin_* -- -D warnings
tests: tests:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
platform: [windows-latest, macos-latest, ubuntu-22.04] platform: [windows-latest, macos-latest, ubuntu-20.04]
feature: [default, dataframe]
include:
# linux CI cannot handle clipboard feature
- default-flags: ""
- platform: ubuntu-20.04
default-flags: "--no-default-features --features=default-no-clipboard"
- feature: default
flags: ""
- feature: dataframe
flags: "--features=dataframe"
exclude:
- platform: windows-latest
feature: dataframe
- platform: macos-latest
feature: dataframe
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.2
- 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.8.0
with:
rustflags: ""
- name: Tests - name: Tests
run: cargo test --workspace --profile ci --exclude nu_plugin_* run: cargo test --workspace --profile ci --exclude nu_plugin_* ${{ matrix.default-flags }} ${{ matrix.flags }}
- name: Check for clean repo - name: Check for clean repo
shell: bash shell: bash
run: | run: |
@ -84,20 +110,22 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
platform: [ubuntu-22.04, macos-latest, windows-latest] platform: [ubuntu-20.04, macos-latest, windows-latest]
py: py:
- py - py
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.2
- 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.8.0
with:
rustflags: ""
- name: Install Nushell - name: Install Nushell
run: cargo install --path . --locked --force run: cargo install --path . --locked --no-default-features
- name: Standard library tests - name: Standard library tests
run: nu -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std' run: nu -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std'
@ -133,19 +161,17 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
# Using macOS 13 runner because 14 is based on the M1 and has half as much RAM (7 GB, platform: [windows-latest, macos-latest, ubuntu-20.04]
# instead of 14 GB) which is too little for us right now.
#
# Failure occurring with clippy for rust 1.77.2
platform: [windows-latest, macos-13, ubuntu-22.04]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.2
- 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.8.0
with:
rustflags: ""
- name: Clippy - name: Clippy
run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS
@ -163,50 +189,3 @@ jobs:
else else
echo "no changes in working directory"; echo "no changes in working directory";
fi fi
wasm:
env:
WASM_OPTIONS: --no-default-features --target wasm32-unknown-unknown
CLIPPY_CONF_DIR: ${{ github.workspace }}/clippy/wasm/
strategy:
matrix:
job:
- name: Build WASM
command: cargo build
args:
- name: Clippy WASM
command: cargo clippy
args: -- $CLIPPY_OPTIONS
name: ${{ matrix.job.name }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
- name: Add wasm32-unknown-unknown target
run: rustup target add wasm32-unknown-unknown
- run: ${{ matrix.job.command }} -p nu-cmd-base $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-cmd-extra $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-cmd-lang $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-color-config $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-command $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-derive-value $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-engine $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-glob $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-json $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-parser $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-path $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-pretty-hex $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-protocol $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-std $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-system $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-table $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-term-grid $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nu-utils $WASM_OPTIONS ${{ matrix.job.args }}
- run: ${{ matrix.job.command }} -p nuon $WASM_OPTIONS ${{ matrix.job.args }}

View File

@ -1,19 +0,0 @@
# 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

View File

@ -1,30 +0,0 @@
# Description:
# - Add milestone to a merged PR automatically
# - Add milestone to a closed issue that has a merged PR fix (if any)
name: Milestone Action
on:
issues:
types: [closed]
pull_request_target:
types: [closed]
jobs:
update-milestone:
runs-on: ubuntu-latest
name: Milestone Update
steps:
- name: Set Milestone for PR
uses: hustcer/milestone-action@main
if: github.event.pull_request.merged == true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Bind milestone to closed issue that has a merged PR fix
- name: Set Milestone for Issue
uses: hustcer/milestone-action@v2
if: github.event.issue.state == 'closed'
with:
action: bind-issue
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -4,7 +4,6 @@
# 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
@ -15,7 +14,6 @@ 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:
@ -27,14 +25,9 @@ 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.1.2
if: github.repository == 'nushell/nightly' if: github.repository == 'nushell/nightly'
with: with:
ref: main ref: main
@ -43,10 +36,10 @@ jobs:
token: ${{ secrets.WORKFLOW_TOKEN }} token: ${{ secrets.WORKFLOW_TOKEN }}
- name: Setup Nushell - name: Setup Nushell
uses: hustcer/setup-nu@v3 uses: hustcer/setup-nu@v3.9
if: github.repository == 'nushell/nightly' if: github.repository == 'nushell/nightly'
with: with:
version: 0.103.0 version: 0.91.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
@ -64,53 +57,16 @@ 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)
- name: Create Tag and Output Tag Name let tag_name = $'nightly-($sha_short)'
if: github.repository == 'nushell/nightly' if (git ls-remote --tags origin $tag_name | is-empty) {
id: vars git tag -a $tag_name -m $'Nightly build from ($sha_short)'
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
} }
release: standard:
name: Nu name: Std
needs: prepare needs: prepare
if: needs.prepare.outputs.skip != 'true'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -122,96 +78,85 @@ jobs:
- x86_64-unknown-linux-gnu - x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl - x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu - aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- armv7-unknown-linux-gnueabihf - armv7-unknown-linux-gnueabihf
- armv7-unknown-linux-musleabihf
- riscv64gc-unknown-linux-gnu - riscv64gc-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_rustflags: ''
- target: x86_64-apple-darwin - target: x86_64-apple-darwin
os: macos-latest os: macos-latest
target_rustflags: ''
- target: x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc
extra: 'bin'
os: windows-latest os: windows-latest
target_rustflags: ''
- target: x86_64-pc-windows-msvc
extra: msi
os: windows-latest
target_rustflags: ''
- target: aarch64-pc-windows-msvc - target: aarch64-pc-windows-msvc
os: windows-11-arm extra: 'bin'
os: windows-latest
target_rustflags: ''
- target: aarch64-pc-windows-msvc
extra: msi
os: windows-latest
target_rustflags: ''
- target: x86_64-unknown-linux-gnu - target: x86_64-unknown-linux-gnu
os: ubuntu-22.04 os: ubuntu-20.04
target_rustflags: ''
- target: x86_64-unknown-linux-musl - target: x86_64-unknown-linux-musl
os: ubuntu-22.04 os: ubuntu-20.04
target_rustflags: ''
- target: aarch64-unknown-linux-gnu - target: aarch64-unknown-linux-gnu
os: ubuntu-22.04 os: ubuntu-20.04
- target: aarch64-unknown-linux-musl target_rustflags: ''
os: ubuntu-22.04
- target: armv7-unknown-linux-gnueabihf - target: armv7-unknown-linux-gnueabihf
os: ubuntu-22.04 os: ubuntu-20.04
- target: armv7-unknown-linux-musleabihf target_rustflags: ''
os: ubuntu-22.04
- target: riscv64gc-unknown-linux-gnu - target: riscv64gc-unknown-linux-gnu
os: ubuntu-22.04 os: ubuntu-latest
- target: loongarch64-unknown-linux-gnu target_rustflags: ''
os: ubuntu-22.04
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4.1.2
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 uses: actions-rust-lang/setup-rust-toolchain@v1.8.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:
rustflags: '' rustflags: ''
- name: Setup Nushell - name: Setup Nushell
uses: hustcer/setup-nu@v3 uses: hustcer/setup-nu@v3.9
if: ${{ matrix.os != 'windows-11-arm' }}
with: with:
version: 0.103.0 version: 0.91.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:
RELEASE_TYPE: standard
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 TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }}
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 uses: JasonEtco/create-an-issue@v2.9.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@ -219,46 +164,139 @@ 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
uses: softprops/action-gh-release@v2.0.9 uses: softprops/action-gh-release@v2.0.4
if: ${{ startsWith(github.repository, 'nushell/nightly') }} if: ${{ startsWith(github.repository, 'nushell/nightly') }}
with: with:
prerelease: true prerelease: true
files: | files: ${{ steps.nu.outputs.archive }}
${{ steps.nu.outputs.msi }} tag_name: nightly-${{ steps.vars.outputs.sha_short }}
${{ steps.nu0.outputs.msi }} name: Nu-nightly-${{ steps.vars.outputs.date }}-${{ steps.vars.outputs.sha_short }}
${{ 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: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
sha256sum: full:
needs: [prepare, release] name: Full
name: Create Sha256sum needs: prepare
runs-on: ubuntu-latest strategy:
if: github.repository == 'nushell/nightly' fail-fast: false
matrix:
target:
- aarch64-apple-darwin
- x86_64-apple-darwin
- x86_64-pc-windows-msvc
- aarch64-pc-windows-msvc
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
extra: ['bin']
include:
- target: aarch64-apple-darwin
os: macos-latest
target_rustflags: '--features=dataframe'
- target: x86_64-apple-darwin
os: macos-latest
target_rustflags: '--features=dataframe'
- target: x86_64-pc-windows-msvc
extra: 'bin'
os: windows-latest
target_rustflags: '--features=dataframe'
- target: x86_64-pc-windows-msvc
extra: msi
os: windows-latest
target_rustflags: '--features=dataframe'
- target: aarch64-pc-windows-msvc
extra: 'bin'
os: windows-latest
target_rustflags: '--features=dataframe'
- target: aarch64-pc-windows-msvc
extra: msi
os: windows-latest
target_rustflags: '--features=dataframe'
- target: x86_64-unknown-linux-gnu
os: ubuntu-20.04
target_rustflags: '--features=dataframe'
- target: x86_64-unknown-linux-musl
os: ubuntu-20.04
target_rustflags: '--features=dataframe'
- target: aarch64-unknown-linux-gnu
os: ubuntu-20.04
target_rustflags: '--features=dataframe'
runs-on: ${{matrix.os}}
steps: steps:
- name: Download Release Archives - uses: actions/checkout@v4.1.2
with:
ref: main
fetch-depth: 0
- name: Update Rust Toolchain Target
run: |
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
rustflags: ''
- name: Setup Nushell
uses: hustcer/setup-nu@v3.9
with:
version: 0.91.0
- name: Release Nu Binary
id: nu
run: nu .github/workflows/release-pkg.nu
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TYPE: full
run: >- OS: ${{ matrix.os }}
gh release download ${{ needs.prepare.outputs.nightly_tag }} REF: ${{ github.ref }}
--repo ${{ github.repository }} TARGET: ${{ matrix.target }}
--pattern '*' _EXTRA_: ${{ matrix.extra }}
--dir release TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }}
- name: Create Checksums
run: cd release && shasum -a 256 * > ../SHA256SUMS - name: Create an Issue for Release Failure
- name: Publish Checksums if: ${{ failure() }}
uses: softprops/action-gh-release@v2.0.9 uses: JasonEtco/create-an-issue@v2.9.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
update_existing: true
search_existing: open
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
# Create a release only in nushell/nightly repo
- name: Publish Archive
uses: softprops/action-gh-release@v2.0.4
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
with: with:
draft: false draft: false
prerelease: true prerelease: true
files: SHA256SUMS name: Nu-nightly-${{ steps.vars.outputs.date }}-${{ steps.vars.outputs.sha_short }}
tag_name: ${{ needs.prepare.outputs.nightly_tag }} tag_name: nightly-${{ steps.vars.outputs.sha_short }}
body: |
This is a NIGHTLY build of Nushell.
It is NOT recommended for production use.
files: ${{ steps.nu.outputs.archive }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -266,24 +304,27 @@ 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:
- uses: actions/checkout@v4 # Sleep for 30 minutes, waiting for the release to be published
- name: Waiting for Release
run: sleep 1800
- uses: actions/checkout@v4.1.2
with: with:
ref: main ref: main
- name: Setup Nushell - name: Setup Nushell
uses: hustcer/setup-nu@v3 uses: hustcer/setup-nu@v3.9
with: with:
version: 0.103.0 version: 0.91.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 | slice $KEEP_COUNT..) let deprecated = (http get https://api.github.com/repos/nushell/nightly/releases | sort-by -r created_at | select tag_name id | range $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

View File

@ -9,6 +9,7 @@
# Instructions for manually creating an MSI for Winget Releases when they fail # Instructions for manually creating an MSI for Winget Releases when they fail
# Added 2022-11-29 when Windows packaging wouldn't work # Added 2022-11-29 when Windows packaging wouldn't work
# Updated again on 2023-02-23 because msis are still failing validation # Updated again on 2023-02-23 because msis are still failing validation
# Update on 2023-10-18 to use RELEASE_TYPE env var to determine if full or not
# To run this manual for windows here are the steps I take # To run this manual for windows here are the steps I take
# checkout the release you want to publish # checkout the release you want to publish
# 1. git checkout 0.86.0 # 1. git checkout 0.86.0
@ -16,26 +17,28 @@
# 2. $env:CARGO_TARGET_DIR = "" # 2. $env:CARGO_TARGET_DIR = ""
# 2. hide-env CARGO_TARGET_DIR # 2. hide-env CARGO_TARGET_DIR
# 3. $env.TARGET = 'x86_64-pc-windows-msvc' # 3. $env.TARGET = 'x86_64-pc-windows-msvc'
# 4. $env.GITHUB_WORKSPACE = 'D:\nushell' # 4. $env.TARGET_RUSTFLAGS = ''
# 5. $env.GITHUB_OUTPUT = 'D:\nushell\output\out.txt' # 5. $env.GITHUB_WORKSPACE = 'D:\nushell'
# 6. $env.OS = 'windows-latest' # 6. $env.GITHUB_OUTPUT = 'D:\nushell\output\out.txt'
# 7. $env.OS = 'windows-latest'
# 8. $env.RELEASE_TYPE = '' # There is full and '' for normal releases
# make sure 7z.exe is in your path https://www.7-zip.org/download.html # make sure 7z.exe is in your path https://www.7-zip.org/download.html
# 7. $env.Path = ($env.Path | append 'c:\apps\7-zip') # 9. $env.Path = ($env.Path | append 'c:\apps\7-zip')
# make sure aria2c.exe is in your path https://github.com/aria2/aria2 # make sure aria2c.exe is in your path https://github.com/aria2/aria2
# 8. $env.Path = ($env.Path | append 'c:\path\to\aria2c') # 10. $env.Path = ($env.Path | append 'c:\path\to\aria2c')
# make sure you have the wixtools installed https://wixtoolset.org/ # make sure you have the wixtools installed https://wixtoolset.org/
# 9. $env.Path = ($env.Path | append 'C:\Users\dschroeder\AppData\Local\tauri\WixTools') # 11. $env.Path = ($env.Path | append 'C:\Users\dschroeder\AppData\Local\tauri\WixTools')
# You need to run the release-pkg twice. The first pass, with _EXTRA_ as 'bin', makes the output # You need to run the release-pkg twice. The first pass, with _EXTRA_ as 'bin', makes the output
# folder and builds everything. The second pass, that generates the msi file, with _EXTRA_ as 'msi' # folder and builds everything. The second pass, that generates the msi file, with _EXTRA_ as 'msi'
# 10. $env._EXTRA_ = 'bin' # 12. $env._EXTRA_ = 'bin'
# 11. source .github\workflows\release-pkg.nu # 13. source .github\workflows\release-pkg.nu
# 12. cd .. # 14. cd ..
# 13. $env._EXTRA_ = 'msi' # 15. $env._EXTRA_ = 'msi'
# 14. source .github\workflows\release-pkg.nu # 16. source .github\workflows\release-pkg.nu
# After msi is generated, you have to update winget-pkgs repo, you'll need to patch the release # After msi is generated, you have to update winget-pkgs repo, you'll need to patch the release
# by deleting the existing msi and uploading this new msi. Then you'll need to update the hash # by deleting the existing msi and uploading this new msi. Then you'll need to update the hash
# on the winget-pkgs PR. To generate the hash, run this command # on the winget-pkgs PR. To generate the hash, run this command
# 15. open target\wix\nu-0.74.0-x86_64-pc-windows-msvc.msi | hash sha256 # 17. open target\wix\nu-0.74.0-x86_64-pc-windows-msvc.msi | hash sha256
# Then, just take the output and put it in the winget-pkgs PR for the hash on the msi # Then, just take the output and put it in the winget-pkgs PR for the hash on the msi
@ -45,15 +48,31 @@ let os = $env.OS
let target = $env.TARGET let target = $env.TARGET
# Repo source dir like `/home/runner/work/nushell/nushell` # Repo source dir like `/home/runner/work/nushell/nushell`
let src = $env.GITHUB_WORKSPACE let src = $env.GITHUB_WORKSPACE
let flags = $env.TARGET_RUSTFLAGS
let dist = $'($env.GITHUB_WORKSPACE)/output' let dist = $'($env.GITHUB_WORKSPACE)/output'
let version = (open Cargo.toml | get package.version) let version = (open Cargo.toml | get package.version)
print $'Debugging info:' print $'Debugging info:'
print { version: $version, bin: $bin, os: $os, target: $target, src: $src, dist: $dist }; hr-line -b print { version: $version, bin: $bin, os: $os, releaseType: $env.RELEASE_TYPE, target: $target, src: $src, flags: $flags, dist: $dist }; hr-line -b
# Rename the full release name so that we won't break the existing scripts for standard release downloading, such as:
# curl -s https://api.github.com/repos/chmln/sd/releases/latest | grep browser_download_url | cut -d '"' -f 4 | grep x86_64-unknown-linux-musl
const FULL_RLS_NAMING = {
x86_64-apple-darwin: 'x86_64-darwin-full',
aarch64-apple-darwin: 'aarch64-darwin-full',
x86_64-unknown-linux-gnu: 'x86_64-linux-gnu-full',
x86_64-pc-windows-msvc: 'x86_64-windows-msvc-full',
x86_64-unknown-linux-musl: 'x86_64-linux-musl-full',
aarch64-unknown-linux-gnu: 'aarch64-linux-gnu-full',
aarch64-pc-windows-msvc: 'aarch64-windows-msvc-full',
riscv64gc-unknown-linux-gnu: 'riscv64-linux-gnu-full',
armv7-unknown-linux-gnueabihf: 'armv7-linux-gnueabihf-full',
}
# $env # $env
let USE_UBUNTU = $os starts-with ubuntu let USE_UBUNTU = $os starts-with ubuntu
let FULL_NAME = $FULL_RLS_NAMING | get -i $target | default 'unknown-target-full'
print $'(char nl)Packaging ($bin) v($version) for ($target) in ($src)...'; hr-line -b print $'(char nl)Packaging ($bin) v($version) for ($target) in ($src)...'; hr-line -b
if not ('Cargo.lock' | path exists) { cargo generate-lockfile } if not ('Cargo.lock' | path exists) { cargo generate-lockfile }
@ -72,44 +91,23 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
'aarch64-unknown-linux-gnu' => { 'aarch64-unknown-linux-gnu' => {
sudo apt-get install gcc-aarch64-linux-gnu -y sudo apt-get install gcc-aarch64-linux-gnu -y
$env.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = 'aarch64-linux-gnu-gcc' $env.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = 'aarch64-linux-gnu-gcc'
cargo-build-nu cargo-build-nu $flags
} }
'riscv64gc-unknown-linux-gnu' => { 'riscv64gc-unknown-linux-gnu' => {
sudo apt-get install gcc-riscv64-linux-gnu -y sudo apt-get install gcc-riscv64-linux-gnu -y
$env.CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER = 'riscv64-linux-gnu-gcc' $env.CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER = 'riscv64-linux-gnu-gcc'
cargo-build-nu cargo-build-nu $flags
} }
'armv7-unknown-linux-gnueabihf' => { 'armv7-unknown-linux-gnueabihf' => {
sudo apt-get install pkg-config gcc-arm-linux-gnueabihf -y sudo apt-get install pkg-config gcc-arm-linux-gnueabihf -y
$env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER = 'arm-linux-gnueabihf-gcc' $env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER = 'arm-linux-gnueabihf-gcc'
cargo-build-nu cargo-build-nu $flags
}
'aarch64-unknown-linux-musl' => {
aria2c https://musl.cc/aarch64-linux-musl-cross.tgz
tar -xf aarch64-linux-musl-cross.tgz -C $env.HOME
$env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.HOME)/aarch64-linux-musl-cross/bin')
$env.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER = 'aarch64-linux-musl-gcc'
cargo-build-nu
}
'armv7-unknown-linux-musleabihf' => {
aria2c https://musl.cc/armv7r-linux-musleabihf-cross.tgz
tar -xf armv7r-linux-musleabihf-cross.tgz -C $env.HOME
$env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.HOME)/armv7r-linux-musleabihf-cross/bin')
$env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER = 'armv7r-linux-musleabihf-gcc'
cargo-build-nu
}
'loongarch64-unknown-linux-gnu' => {
aria2c https://github.com/loongson/build-tools/releases/download/2024.08.08/x86_64-cross-tools-loongarch64-binutils_2.43-gcc_14.2.0-glibc_2.40.tar.xz
tar xf x86_64-cross-tools-loongarch64-*.tar.xz
$env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.PWD)/cross-tools/bin')
$env.CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_GNU_LINKER = 'loongarch64-unknown-linux-gnu-gcc'
cargo-build-nu
} }
_ => { _ => {
# musl-tools to fix 'Failed to find tool. Is `musl-gcc` installed?' # musl-tools to fix 'Failed to find tool. Is `musl-gcc` installed?'
# Actually just for x86_64-unknown-linux-musl target # Actually just for x86_64-unknown-linux-musl target
if $USE_UBUNTU { sudo apt install musl-tools -y } if $USE_UBUNTU { sudo apt install musl-tools -y }
cargo-build-nu cargo-build-nu $flags
} }
} }
} }
@ -117,14 +115,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 =~ 'windows' { if $os in ['windows-latest'] {
cargo-build-nu cargo-build-nu $flags
} }
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Prepare for the release archive # Prepare for the release archive
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
let suffix = if $os =~ 'windows' { '.exe' } let suffix = if $os == 'windows-latest' { '.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)'
@ -136,22 +134,16 @@ print $'(char nl)All executable files:'; hr-line
print (ls -f ($executable | into glob)); sleep 1sec print (ls -f ($executable | into glob)); sleep 1sec
print $'(char nl)Copying release files...'; hr-line print $'(char nl)Copying release files...'; hr-line
"To use the included Nushell plugins, register the binaries with the `plugin add` command to tell Nu where to find the plugin. "To use Nu plugins, use the register command to tell Nu where to find the plugin. For example:
Then you can use `plugin use` to load the plugin into your session.
For example:
> plugin add ./nu_plugin_query > register ./nu_plugin_query" | save $'($dist)/README.txt' -f
> plugin use query
For more information, refer to https://www.nushell.sh/book/plugins.html
" | save $'($dist)/README.txt' -f
[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' { let ver = if $os == 'windows-latest' {
(do -i { .\output\nu.exe -c 'version' }) | default '' | str join (do -i { .\output\nu.exe -c 'version' }) | str join
} else { } else {
(do -i { ./output/nu -c 'version' }) | default '' | str join (do -i { ./output/nu -c 'version' }) | 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)'
@ -164,7 +156,7 @@ cd $dist; print $'(char nl)Creating release archive...'; hr-line
if $os in ['macos-latest'] or $USE_UBUNTU { if $os in ['macos-latest'] or $USE_UBUNTU {
let files = (ls | get name) let files = (ls | get name)
let dest = $'($bin)-($version)-($target)' let dest = if $env.RELEASE_TYPE == 'full' { $'($bin)-($version)-($FULL_NAME)' } else { $'($bin)-($version)-($target)' }
let archive = $'($dist)/($dest).tar.gz' let archive = $'($dist)/($dest).tar.gz'
mkdir $dest mkdir $dest
@ -175,63 +167,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)(char nl)" o>> $env.GITHUB_OUTPUT echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
} else if $os =~ 'windows' { } else if $os == 'windows-latest' {
let releaseStem = $'($bin)-($version)-($target)' let releaseStem = if $env.RELEASE_TYPE == 'full' { $'($bin)-($version)-($FULL_NAME)' } else { $'($bin)-($version)-($target)' }
let arch = if $nu.os-info.arch =~ 'x86_64' { 'x64' } else { 'arm64' }
fetch-less $arch
print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls | print print $'(char nl)Download less related stuffs...'; hr-line
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 *) aria2c https://raw.githubusercontent.com/jftuga/less-Windows/master/LICENSE -o LICENSE-for-less.txt
let pkg = (ls -f $archive | get name)
if not ($pkg | is-empty) {
# Workaround for https://github.com/softprops/action-gh-release/issues/280
let archive = ($pkg | get 0 | str replace --all '\' '/')
print $'archive: ---> ($archive)'
echo $"archive=($archive)(char nl)" o>> $env.GITHUB_OUTPUT
}
# Create extra Windows msi release package if dotnet and wix are available # Create Windows msi release package
let installed = [dotnet wix] | all { (which $in | length) > 0 } if (get-env _EXTRA_) == 'msi' {
if $installed and (wix --version | split row . | first | into int) >= 6 {
let wixRelease = $'($src)/target/wix/($releaseStem).msi'
print $'(char nl)Start creating Windows msi package with the following contents...' print $'(char nl)Start creating Windows msi package with the following contents...'
cd $src; cd wix; hr-line; mkdir nu cd $src; hr-line
# Wix need the binaries be stored in nu folder # Wix need the binaries be stored in target/release/
cp -r ($'($dist)/*' | into glob) nu/ cp -r ($'($dist)/*' | into glob) target/release/
cp $'($dist)/README.txt' . ls target/release/* | print
ls -f nu/* | print cargo install cargo-wix --version 0.3.4
./nu/nu.exe -c $'NU_RELEASE_VERSION=($version) dotnet build -c Release -p:Platform=($arch)' cargo wix --no-build --nocapture --package nu --output $wixRelease
glob **/*.msi | print
# Workaround for https://github.com/softprops/action-gh-release/issues/280 # Workaround for https://github.com/softprops/action-gh-release/issues/280
let wixRelease = (glob **/*.msi | where $it =~ bin | get 0 | str replace --all '\' '/') let archive = ($wixRelease | str replace --all '\' '/')
let msi = $'($wixRelease | path dirname)/nu-($version)-($target).msi' print $'archive: ---> ($archive)';
mv $wixRelease $msi echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
print $'MSI archive: ---> ($msi)';
echo $"msi=($msi)(char nl)" o>> $env.GITHUB_OUTPUT } else {
print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls | print
let archive = $'($dist)/($releaseStem).zip'
7z a $archive ...(glob *)
let pkg = (ls -f $archive | get name)
if not ($pkg | is-empty) {
# Workaround for https://github.com/softprops/action-gh-release/issues/280
let archive = ($pkg | get 0 | str replace --all '\' '/')
print $'archive: ---> ($archive)'
echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
}
} }
} }
def fetch-less [ def 'cargo-build-nu' [ options: string ] {
arch: string = 'x64' # The architecture to fetch if ($options | str trim | is-empty) {
] { if $os == 'windows-latest' {
let less_zip = $'less-($arch).zip' cargo build --release --all --target $target
print $'Fetching less archive: (ansi g)($less_zip)(ansi reset)' } else {
let url = $'https://github.com/jftuga/less-Windows/releases/download/less-v668/($less_zip)' cargo build --release --all --target $target --features=static-link-openssl
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' [] {
if $os =~ 'windows' {
cargo build --release --all --target $target
} else { } else {
cargo build --release --all --target $target --features=static-link-openssl if $os == 'windows-latest' {
cargo build --release --all --target $target $options
} else {
cargo build --release --all --target $target --features=static-link-openssl $options
}
} }
} }

View File

@ -7,17 +7,15 @@ name: Create Release Draft
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
tags: tags: ["[0-9]+.[0-9]+.[0-9]+*"]
- '[0-9]+.[0-9]+.[0-9]+*'
- '!*nightly*' # Don't trigger release for nightly tags
defaults: defaults:
run: run:
shell: bash shell: bash
jobs: jobs:
release: standard:
name: Nu name: Std
strategy: strategy:
fail-fast: false fail-fast: false
@ -30,126 +28,176 @@ jobs:
- x86_64-unknown-linux-gnu - x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl - x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu - aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- armv7-unknown-linux-gnueabihf - armv7-unknown-linux-gnueabihf
- armv7-unknown-linux-musleabihf
- riscv64gc-unknown-linux-gnu - riscv64gc-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_rustflags: ''
- target: x86_64-apple-darwin - target: x86_64-apple-darwin
os: macos-latest os: macos-latest
target_rustflags: ''
- target: x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc
extra: 'bin'
os: windows-latest os: windows-latest
target_rustflags: ''
- target: x86_64-pc-windows-msvc
extra: msi
os: windows-latest
target_rustflags: ''
- target: aarch64-pc-windows-msvc - target: aarch64-pc-windows-msvc
os: windows-11-arm extra: 'bin'
os: windows-latest
target_rustflags: ''
- target: aarch64-pc-windows-msvc
extra: msi
os: windows-latest
target_rustflags: ''
- target: x86_64-unknown-linux-gnu - target: x86_64-unknown-linux-gnu
os: ubuntu-22.04 os: ubuntu-20.04
target_rustflags: ''
- target: x86_64-unknown-linux-musl - target: x86_64-unknown-linux-musl
os: ubuntu-22.04 os: ubuntu-20.04
target_rustflags: ''
- target: aarch64-unknown-linux-gnu - target: aarch64-unknown-linux-gnu
os: ubuntu-22.04 os: ubuntu-20.04
- target: aarch64-unknown-linux-musl target_rustflags: ''
os: ubuntu-22.04
- target: armv7-unknown-linux-gnueabihf - target: armv7-unknown-linux-gnueabihf
os: ubuntu-22.04 os: ubuntu-20.04
- target: armv7-unknown-linux-musleabihf target_rustflags: ''
os: ubuntu-22.04
- target: riscv64gc-unknown-linux-gnu - target: riscv64gc-unknown-linux-gnu
os: ubuntu-22.04 os: ubuntu-latest
- target: loongarch64-unknown-linux-gnu target_rustflags: ''
os: ubuntu-22.04
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4.1.2
- 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.12.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.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
rustflags: '' rustflags: ''
- name: Setup Nushell - name: Setup Nushell
uses: hustcer/setup-nu@v3 uses: hustcer/setup-nu@v3.9
if: ${{ matrix.os != 'windows-11-arm' }}
with: with:
version: 0.103.0 version: 0.91.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:
RELEASE_TYPE: standard
OS: ${{ matrix.os }} OS: ${{ matrix.os }}
REF: ${{ github.ref }} REF: ${{ github.ref }}
TARGET: ${{ matrix.target }} TARGET: ${{ matrix.target }}
_EXTRA_: ${{ matrix.extra }}
TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }}
- name: Build Nu for Windows ARM64 # REF: https://github.com/marketplace/actions/gh-release
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.
# See: https://github.com/softprops/action-gh-release/issues/445
- name: Publish Archive - name: Publish Archive
uses: softprops/action-gh-release@v2.0.5 uses: softprops/action-gh-release@v2.0.4
if: ${{ startsWith(github.ref, 'refs/tags/') }} if: ${{ startsWith(github.ref, 'refs/tags/') }}
with: with:
draft: true draft: true
files: | files: ${{ steps.nu.outputs.archive }}
${{ 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 }}
sha256sum: full:
needs: release name: Full
name: Create Sha256sum
runs-on: ubuntu-latest strategy:
fail-fast: false
matrix:
target:
- aarch64-apple-darwin
- x86_64-apple-darwin
- x86_64-pc-windows-msvc
- aarch64-pc-windows-msvc
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
extra: ['bin']
include:
- target: aarch64-apple-darwin
os: macos-latest
target_rustflags: '--features=dataframe'
- target: x86_64-apple-darwin
os: macos-latest
target_rustflags: '--features=dataframe'
- target: x86_64-pc-windows-msvc
extra: 'bin'
os: windows-latest
target_rustflags: '--features=dataframe'
- target: x86_64-pc-windows-msvc
extra: msi
os: windows-latest
target_rustflags: '--features=dataframe'
- target: aarch64-pc-windows-msvc
extra: 'bin'
os: windows-latest
target_rustflags: '--features=dataframe'
- target: aarch64-pc-windows-msvc
extra: msi
os: windows-latest
target_rustflags: '--features=dataframe'
- target: x86_64-unknown-linux-gnu
os: ubuntu-20.04
target_rustflags: '--features=dataframe'
- target: x86_64-unknown-linux-musl
os: ubuntu-20.04
target_rustflags: '--features=dataframe'
- target: aarch64-unknown-linux-gnu
os: ubuntu-20.04
target_rustflags: '--features=dataframe'
runs-on: ${{matrix.os}}
steps: steps:
- name: Download Release Archives - uses: actions/checkout@v4.1.2
- name: Update Rust Toolchain Target
run: |
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
cache: false
rustflags: ''
- name: Setup Nushell
uses: hustcer/setup-nu@v3.9
with:
version: 0.91.0
- name: Release Nu Binary
id: nu
run: nu .github/workflows/release-pkg.nu
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TYPE: full
run: >- OS: ${{ matrix.os }}
gh release download ${{ github.ref_name }} REF: ${{ github.ref }}
--repo ${{ github.repository }} TARGET: ${{ matrix.target }}
--pattern '*' _EXTRA_: ${{ matrix.extra }}
--dir release TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }}
- name: Create Checksums
run: cd release && shasum -a 256 * > ../SHA256SUMS # REF: https://github.com/marketplace/actions/gh-release
- name: Publish Checksums - name: Publish Archive
uses: softprops/action-gh-release@v2.0.5 uses: softprops/action-gh-release@v2.0.4
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with: with:
draft: true draft: true
files: SHA256SUMS files: ${{ steps.nu.outputs.archive }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Actions Repository - name: Checkout Actions Repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.2
- name: Check spelling - name: Check spelling
uses: crate-ci/typos@v1.31.1 uses: crate-ci/typos@v1.20.3

View File

@ -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: nushell fork-user: fdncred

View File

@ -1,26 +0,0 @@
cff-version: 1.2.0
title: 'Nushell'
message: >-
If you use this software and wish to cite it,
you can use the metadata from this file.
type: software
authors:
- name: "The Nushell Project Team"
identifiers:
- type: url
value: 'https://github.com/nushell/nushell'
description: Repository
repository-code: 'https://github.com/nushell/nushell'
url: 'https://www.nushell.sh/'
abstract: >-
The goal of the Nushell project is to take the Unix
philosophy of shells, where pipes connect simple commands
together, and bring it to the modern style of development.
Thus, rather than being either a shell, or a programming
language, Nushell connects both by bringing a rich
programming language and a full-featured shell together
into one package.
keywords:
- nushell
- shell
license: MIT

View File

@ -55,6 +55,7 @@ It is good practice to cover your changes with a test. Also, try to think about
Tests can be found in different places: Tests can be found in different places:
* `/tests` * `/tests`
* `src/tests`
* command examples * command examples
* crate-specific tests * crate-specific tests
@ -71,6 +72,11 @@ Read cargo's documentation for more details: https://doc.rust-lang.org/cargo/ref
cargo run cargo run
``` ```
- Build and run with dataframe support.
```nushell
cargo run --features=dataframe
```
- Run Clippy on Nushell: - Run Clippy on Nushell:
```nushell ```nushell
@ -88,6 +94,11 @@ Read cargo's documentation for more details: https://doc.rust-lang.org/cargo/ref
cargo test --workspace cargo test --workspace
``` ```
along with dataframe tests
```nushell
cargo test --workspace --features=dataframe
```
or via the `toolkit.nu` command: or via the `toolkit.nu` command:
```nushell ```nushell
use toolkit.nu test use toolkit.nu test
@ -230,7 +241,7 @@ You can help us to make the review process a smooth experience:
- Choose what simplifies having confidence in the conflict resolution and the review. **Merge commits in your branch are OK** in the squash model. - Choose what simplifies having confidence in the conflict resolution and the review. **Merge commits in your branch are OK** in the squash model.
- Feel free to notify your reviewers or affected PR authors if your change might cause larger conflicts with another change. - Feel free to notify your reviewers or affected PR authors if your change might cause larger conflicts with another change.
- During the rollup of multiple PRs, we may choose to resolve merge conflicts and CI failures ourselves. (Allow maintainers to push to your branch to enable us to do this quickly.) - During the rollup of multiple PRs, we may choose to resolve merge conflicts and CI failures ourselves. (Allow maintainers to push to your branch to enable us to do this quickly.)
## License ## License
We use the [MIT License](https://github.com/nushell/nushell/blob/main/LICENSE) in all of our Nushell projects. If you are including or referencing a crate that uses the [GPL License](https://www.gnu.org/licenses/gpl-3.0.en.html#license-text) unfortunately we will not be able to accept your PR. We use the [MIT License](https://github.com/nushell/nushell/blob/main/LICENSE) in all of our Nushell projects. If you are including or referencing a crate that uses the [GPL License](https://www.gnu.org/licenses/gpl-3.0.en.html#license-text) unfortunately we will not be able to accept your PR.

5366
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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.84.1" rust-version = "1.77.2"
version = "0.104.1" version = "0.92.2"
# 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
@ -31,7 +31,7 @@ members = [
"crates/nu-cmd-base", "crates/nu-cmd-base",
"crates/nu-cmd-extra", "crates/nu-cmd-extra",
"crates/nu-cmd-lang", "crates/nu-cmd-lang",
"crates/nu-cmd-plugin", "crates/nu-cmd-dataframe",
"crates/nu-command", "crates/nu-command",
"crates/nu-color-config", "crates/nu-color-config",
"crates/nu-explore", "crates/nu-explore",
@ -39,11 +39,7 @@ members = [
"crates/nu-lsp", "crates/nu-lsp",
"crates/nu-pretty-hex", "crates/nu-pretty-hex",
"crates/nu-protocol", "crates/nu-protocol",
"crates/nu-derive-value",
"crates/nu-plugin", "crates/nu-plugin",
"crates/nu-plugin-core",
"crates/nu-plugin-engine",
"crates/nu-plugin-protocol",
"crates/nu-plugin-test-support", "crates/nu-plugin-test-support",
"crates/nu_plugin_inc", "crates/nu_plugin_inc",
"crates/nu_plugin_gstat", "crates/nu_plugin_gstat",
@ -51,176 +47,147 @@ members = [
"crates/nu_plugin_query", "crates/nu_plugin_query",
"crates/nu_plugin_custom_values", "crates/nu_plugin_custom_values",
"crates/nu_plugin_formats", "crates/nu_plugin_formats",
"crates/nu_plugin_polars",
"crates/nu_plugin_stress_internals",
"crates/nu-std", "crates/nu-std",
"crates/nu-table", "crates/nu-table",
"crates/nu-term-grid", "crates/nu-term-grid",
"crates/nu-test-support", "crates/nu-test-support",
"crates/nu-utils", "crates/nu-utils",
"crates/nuon",
] ]
[workspace.dependencies] [workspace.dependencies]
alphanumeric-sort = "1.5" alphanumeric-sort = "1.5"
ansi-str = "0.8" ansi-str = "0.8"
anyhow = "1.0.82" base64 = "0.22"
base64 = "0.22.1" bracoxide = "0.1.2"
bracoxide = "0.1.5"
brotli = "7.0"
byteorder = "1.5" byteorder = "1.5"
bytes = "1" bytesize = "1.3"
bytesize = "1.3.3" calamine = "0.24.0"
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" }
chrono-humanize = "0.2.3" chrono-humanize = "0.2.3"
chrono-tz = "0.10" chrono-tz = "0.8"
crossbeam-channel = "0.5.8" crossbeam-channel = "0.5.8"
crossterm = "0.28.1" crossterm = "0.27"
csv = "1.3" csv = "1.3"
ctrlc = "3.4" ctrlc = "3.4"
devicons = "0.6.12"
dialoguer = { default-features = false, version = "0.11" } dialoguer = { default-features = false, version = "0.11" }
digest = { default-features = false, version = "0.10" } digest = { default-features = false, version = "0.10" }
dirs = "5.0" dirs-next = "2.0"
dirs-sys = "0.4"
dtparse = "2.0" dtparse = "2.0"
encoding_rs = "0.8" encoding_rs = "0.8"
fancy-regex = "0.14" fancy-regex = "0.13"
filesize = "0.2" filesize = "0.2"
filetime = "0.2" filetime = "0.2"
fs_extra = "1.3"
fuzzy-matcher = "0.3"
hamcrest2 = "0.3"
heck = "0.5.0" heck = "0.5.0"
human-date-parser = "0.3.0" human-date-parser = "0.1.1"
indexmap = "2.9" indexmap = "2.2"
indicatif = "0.17" indicatif = "0.17"
interprocess = "2.2.0"
is_executable = "1.0" is_executable = "1.0"
itertools = "0.13" itertools = "0.12"
libc = "0.2" libc = "0.2"
libproc = "0.14" libproc = "0.14"
log = "0.4" log = "0.4"
lru = "0.12" lru = "0.12"
lscolors = { version = "0.17", default-features = false } lscolors = { version = "0.17", default-features = false }
lsp-server = "0.7.8" lsp-server = "0.7.5"
lsp-types = { version = "0.97.0", features = ["proposed"] } lsp-types = "0.95.0"
lsp-textdocument = "0.4.2"
mach2 = "0.4" mach2 = "0.4"
md5 = { version = "0.10", package = "md-5" } md5 = { version = "0.10", package = "md-5"}
miette = "7.5" miette = "7.2"
mime = "0.3.17" mime = "0.3"
mime_guess = "2.0" mime_guess = "2.0"
mockito = { version = "1.7", default-features = false } mockito = { version = "1.4", default-features = false }
multipart-rs = "0.1.13"
native-tls = "0.2" native-tls = "0.2"
nix = { version = "0.29", default-features = false } nix = { version = "0.28", default-features = false }
notify-debouncer-full = { version = "0.3", default-features = false } notify-debouncer-full = { version = "0.3", default-features = false }
nu-ansi-term = "0.50.1" nu-ansi-term = "0.50.0"
nucleo-matcher = "0.3"
num-format = "0.4" num-format = "0.4"
num-traits = "0.2" num-traits = "0.2"
oem_cp = "2.0.0"
omnipath = "0.1" omnipath = "0.1"
open = "5.3" once_cell = "1.18"
os_pipe = { version = "1.2", features = ["io_safety"] } open = "5.1"
os_pipe = "1.1"
pathdiff = "0.2" pathdiff = "0.2"
percent-encoding = "2" percent-encoding = "2"
pretty_assertions = "1.4"
print-positions = "0.6" print-positions = "0.6"
proc-macro-error2 = "2.0" procfs = "0.16.0"
proc-macro2 = "1.0"
procfs = "0.17.0"
pwd = "1.3" pwd = "1.3"
quick-xml = "0.37.0" quick-xml = "0.31.0"
quickcheck = "1.0" quickcheck = "1.0"
quickcheck_macros = "1.0" quickcheck_macros = "1.0"
quote = "1.0" rand = "0.8"
rand = "0.9" ratatui = "0.26"
getrandom = "0.2" # pick same version that rand requires
rand_chacha = "0.9"
ratatui = "0.29"
rayon = "1.10" rayon = "1.10"
reedline = "0.40.0" reedline = "0.31.0"
rmp = "0.8" regex = "1.9.5"
rmp-serde = "1.3" ropey = "1.6.1"
roxmltree = "0.20" roxmltree = "0.19"
rstest = { version = "0.23", default-features = false } rstest = { version = "0.18", default-features = false }
rstest_reuse = "0.7"
rusqlite = "0.31" rusqlite = "0.31"
rust-embed = "8.7.0" rust-embed = "8.2.0"
scopeguard = { version = "1.2.0" } same-file = "1.0"
serde = { version = "1.0" } serde = { version = "1.0", default-features = false }
serde_json = "1.0.97" serde_json = "1.0"
serde_urlencoded = "0.7.1" serde_urlencoded = "0.7.1"
serde_yaml = "0.9.33" serde_yaml = "0.9"
sha2 = "0.10" sha2 = "0.10"
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.2.0"
strum = "0.26" sysinfo = "0.30"
strum_macros = "0.26" tabled = { version = "0.14.0", default-features = false }
syn = "2.0" tempfile = "3.10"
sysinfo = "0.33" terminal_size = "0.3"
tabled = { version = "0.17.0", default-features = false } titlecase = "2.0"
tempfile = "3.15"
titlecase = "3.5"
toml = "0.8" toml = "0.8"
trash = "5.2" trash = "3.3"
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.11"
unicode-width = "0.2" unicode-width = "0.1"
ureq = { version = "2.12", default-features = false, features = ["socks-proxy"] } ureq = { version = "2.9", default-features = false }
url = "2.2" url = "2.2"
uu_cp = "0.0.30" uu_cp = "0.0.25"
uu_mkdir = "0.0.30" uu_mkdir = "0.0.25"
uu_mktemp = "0.0.30" uu_mktemp = "0.0.25"
uu_mv = "0.0.30" uu_mv = "0.0.25"
uu_touch = "0.0.30" uu_whoami = "0.0.25"
uu_whoami = "0.0.30" uu_uname = "0.0.25"
uu_uname = "0.0.30" uucore = "0.0.25"
uucore = "0.0.30" uuid = "1.8.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" which = "6.0.0"
which = "7.0.0" windows = "0.54"
windows = "0.56"
windows-sys = "0.48"
winreg = "0.52" winreg = "0.52"
memchr = "2.7.4"
[workspace.lints.clippy]
# Warning: workspace lints affect library code as well as tests, so don't enable lints that would be too noisy in tests like that.
# todo = "warn"
unchecked_duration_subtraction = "warn"
[lints]
workspace = true
[dependencies] [dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.104.1" } nu-cli = { path = "./crates/nu-cli", version = "0.92.2" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.104.1" } nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.92.2" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.104.1" } nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.92.2" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.104.1", optional = true } nu-cmd-dataframe = { path = "./crates/nu-cmd-dataframe", version = "0.92.2", features = [
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.104.1" } "dataframe",
nu-command = { path = "./crates/nu-command", version = "0.104.1" } ], optional = true }
nu-engine = { path = "./crates/nu-engine", version = "0.104.1" } nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.92.2" }
nu-explore = { path = "./crates/nu-explore", version = "0.104.1" } nu-command = { path = "./crates/nu-command", version = "0.92.2" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.104.1" } nu-engine = { path = "./crates/nu-engine", version = "0.92.2" }
nu-parser = { path = "./crates/nu-parser", version = "0.104.1" } nu-explore = { path = "./crates/nu-explore", version = "0.92.2" }
nu-path = { path = "./crates/nu-path", version = "0.104.1" } nu-lsp = { path = "./crates/nu-lsp/", version = "0.92.2" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.104.1" } nu-parser = { path = "./crates/nu-parser", version = "0.92.2" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.104.1" } nu-path = { path = "./crates/nu-path", version = "0.92.2" }
nu-std = { path = "./crates/nu-std", version = "0.104.1" } nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.92.2" }
nu-system = { path = "./crates/nu-system", version = "0.104.1" } nu-protocol = { path = "./crates/nu-protocol", version = "0.92.2" }
nu-utils = { path = "./crates/nu-utils", version = "0.104.1" } nu-std = { path = "./crates/nu-std", version = "0.92.2" }
nu-system = { path = "./crates/nu-system", version = "0.92.2" }
nu-utils = { path = "./crates/nu-utils", version = "0.92.2" }
reedline = { workspace = true, features = ["bashisms", "sqlite"] } reedline = { workspace = true, features = ["bashisms", "sqlite"] }
crossterm = { workspace = true } crossterm = { workspace = true }
ctrlc = { workspace = true } ctrlc = { workspace = true }
dirs = { workspace = true }
log = { workspace = true } log = { workspace = true }
miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] } miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] }
multipart-rs = { workspace = true } mimalloc = { version = "0.1.37", default-features = false, optional = true }
serde_json = { workspace = true } serde_json = { workspace = true }
simplelog = "0.12" simplelog = "0.12"
time = "0.3" time = "0.3"
@ -241,59 +208,54 @@ nix = { workspace = true, default-features = false, features = [
] } ] }
[dev-dependencies] [dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", version = "0.104.1" } nu-test-support = { path = "./crates/nu-test-support", version = "0.92.2" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.104.1" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.104.1" }
assert_cmd = "2.0" assert_cmd = "2.0"
dirs = { workspace = true } dirs-next = { workspace = true }
tango-bench = "0.6" divan = "0.1.14"
pretty_assertions = { workspace = true } pretty_assertions = "1.4"
fancy-regex = { workspace = true }
rstest = { workspace = true, default-features = false } rstest = { workspace = true, default-features = false }
serial_test = "3.2" serial_test = "3.0"
tempfile = { workspace = true } tempfile = { workspace = true }
[features] [features]
plugin = [ plugin = [
# crates "nu-plugin",
"nu-cmd-plugin",
"nu-plugin-engine",
# features
"nu-cli/plugin", "nu-cli/plugin",
"nu-cmd-lang/plugin",
"nu-command/plugin",
"nu-engine/plugin",
"nu-engine/plugin",
"nu-parser/plugin", "nu-parser/plugin",
"nu-command/plugin",
"nu-protocol/plugin", "nu-protocol/plugin",
"nu-engine/plugin",
] ]
default = ["default-no-clipboard", "system-clipboard"]
default = [ # Enables convenient omitting of the system-clipboard feature, as it leads to problems in ci on linux
# See https://github.com/nushell/nushell/pull/11535
default-no-clipboard = [
"plugin", "plugin",
"which-support",
"trash-support", "trash-support",
"sqlite", "sqlite",
"mimalloc",
] ]
stable = ["default"] stable = ["default"]
wasi = ["nu-cmd-lang/wasi"]
# NOTE: individual features are also passed to `nu-cmd-lang` that uses them to generate the feature matrix in the `version` command # NOTE: individual features are also passed to `nu-cmd-lang` that uses them to generate the feature matrix in the `version` command
# Enable to statically link OpenSSL (perl is required, to build OpenSSL https://docs.rs/openssl/latest/openssl/); # Enable to statically link OpenSSL (perl is required, to build OpenSSL https://docs.rs/openssl/latest/openssl/);
# otherwise the system version will be used. Not enabled by default because it takes a while to build # otherwise the system version will be used. Not enabled by default because it takes a while to build
static-link-openssl = ["dep:openssl", "nu-cmd-lang/static-link-openssl"] static-link-openssl = ["dep:openssl", "nu-cmd-lang/static-link-openssl"]
# Optional system clipboard support in `reedline`, this behavior has problematic compatibility with some systems. mimalloc = ["nu-cmd-lang/mimalloc", "dep:mimalloc"]
# Missing X server/ Wayland can cause issues system-clipboard = ["reedline/system_clipboard", "nu-cli/system-clipboard"]
system-clipboard = [
"reedline/system_clipboard",
"nu-cli/system-clipboard",
"nu-cmd-lang/system-clipboard",
]
# Stable (Default) # Stable (Default)
which-support = ["nu-command/which-support", "nu-cmd-lang/which-support"]
trash-support = ["nu-command/trash-support", "nu-cmd-lang/trash-support"] trash-support = ["nu-command/trash-support", "nu-cmd-lang/trash-support"]
# Dataframe feature for nushell
dataframe = ["dep:nu-cmd-dataframe", "nu-cmd-lang/dataframe"]
# SQLite commands for nushell # SQLite commands for nushell
sqlite = ["nu-command/sqlite", "nu-cmd-lang/sqlite", "nu-std/sqlite"] sqlite = ["nu-command/sqlite", "nu-cmd-lang/sqlite"]
[profile.release] [profile.release]
opt-level = "s" # Optimize for size opt-level = "s" # Optimize for size

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 - 2025 The Nushell Project Developers Copyright (c) 2019 - 2023 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

122
README.md
View File

@ -4,6 +4,7 @@
[![Nightly Build](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml/badge.svg)](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml) [![Nightly Build](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml/badge.svg)](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml)
[![Discord](https://img.shields.io/discord/601130461678272522.svg?logo=discord)](https://discord.gg/NtAbbGn) [![Discord](https://img.shields.io/discord/601130461678272522.svg?logo=discord)](https://discord.gg/NtAbbGn)
[![The Changelog #363](https://img.shields.io/badge/The%20Changelog-%23363-61c192.svg)](https://changelog.com/podcast/363) [![The Changelog #363](https://img.shields.io/badge/The%20Changelog-%23363-61c192.svg)](https://changelog.com/podcast/363)
[![@nu_shell](https://img.shields.io/badge/twitter-@nu_shell-1DA1F3?style=flat-square)](https://twitter.com/nu_shell)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/nushell/nushell)](https://github.com/nushell/nushell/graphs/commit-activity) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/nushell/nushell)](https://github.com/nushell/nushell/graphs/commit-activity)
[![GitHub contributors](https://img.shields.io/github/contributors/nushell/nushell)](https://github.com/nushell/nushell/graphs/contributors) [![GitHub contributors](https://img.shields.io/github/contributors/nushell/nushell)](https://github.com/nushell/nushell/graphs/contributors)
@ -34,7 +35,7 @@ This project has reached a minimum-viable-product level of quality. Many people
The [Nushell book](https://www.nushell.sh/book/) is the primary source of Nushell documentation. You can find [a full list of Nu commands in the book](https://www.nushell.sh/commands/), and we have many examples of using Nu in our [cookbook](https://www.nushell.sh/cookbook/). The [Nushell book](https://www.nushell.sh/book/) is the primary source of Nushell documentation. You can find [a full list of Nu commands in the book](https://www.nushell.sh/commands/), and we have many examples of using Nu in our [cookbook](https://www.nushell.sh/cookbook/).
We're also active on [Discord](https://discord.gg/NtAbbGn); come and chat with us! We're also active on [Discord](https://discord.gg/NtAbbGn) and [Twitter](https://twitter.com/nu_shell); come and chat with us!
## Installation ## Installation
@ -51,13 +52,13 @@ To use `Nu` in GitHub Action, check [setup-nu](https://github.com/marketplace/ac
Detailed installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). Nu is available via many package managers: Detailed installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). Nu is available via many package managers:
[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg?columns=3)](https://repology.org/project/nushell/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg)](https://repology.org/project/nushell/versions)
For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md). For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md).
## Configuration ## Configuration
The default configurations can be found at [sample_config](crates/nu-utils/src/default_files) The default configurations can be found at [sample_config](crates/nu-utils/src/sample_config)
which are the configuration files one gets when they startup Nushell for the first time. which are the configuration files one gets when they startup Nushell for the first time.
It sets all of the default configuration to run Nushell. From here one can It sets all of the default configuration to run Nushell. From here one can
@ -94,44 +95,44 @@ Commands that work in the pipeline fit into one of three categories:
Commands are separated by the pipe symbol (`|`) to denote a pipeline flowing left to right. Commands are separated by the pipe symbol (`|`) to denote a pipeline flowing left to right.
```shell ```shell
ls | where type == "dir" | table > ls | where type == "dir" | table
# => ╭────┬──────────┬──────┬─────────┬───────────────╮ ╭────┬──────────┬──────┬─────────┬───────────────╮
# => │ # │ name │ type │ size │ modified │ # │ name │ type │ size │ modified │
# => ├────┼──────────┼──────┼─────────┼───────────────┤ ├────┼──────────┼──────┼─────────┼───────────────┤
# => │ 0 │ .cargo │ dir │ 0 B │ 9 minutes ago │ 0 │ .cargo │ dir │ 0 B │ 9 minutes ago │
# => │ 1 │ assets │ dir │ 0 B │ 2 weeks ago │ 1 │ assets │ dir │ 0 B │ 2 weeks ago │
# => │ 2 │ crates │ dir │ 4.0 KiB │ 2 weeks ago │ 2 │ crates │ dir │ 4.0 KiB │ 2 weeks ago │
# => │ 3 │ docker │ dir │ 0 B │ 2 weeks ago │ 3 │ docker │ dir │ 0 B │ 2 weeks ago │
# => │ 4 │ docs │ dir │ 0 B │ 2 weeks ago │ 4 │ docs │ dir │ 0 B │ 2 weeks ago │
# => │ 5 │ images │ dir │ 0 B │ 2 weeks ago │ 5 │ images │ dir │ 0 B │ 2 weeks ago │
# => │ 6 │ pkg_mgrs │ dir │ 0 B │ 2 weeks ago │ 6 │ pkg_mgrs │ dir │ 0 B │ 2 weeks ago │
# => │ 7 │ samples │ dir │ 0 B │ 2 weeks ago │ 7 │ samples │ dir │ 0 B │ 2 weeks ago │
# => │ 8 │ src │ dir │ 4.0 KiB │ 2 weeks ago │ 8 │ src │ dir │ 4.0 KiB │ 2 weeks ago │
# => │ 9 │ target │ dir │ 0 B │ a day ago │ 9 │ target │ dir │ 0 B │ a day ago │
# => │ 10 │ tests │ dir │ 4.0 KiB │ 2 weeks ago │ 10 │ tests │ dir │ 4.0 KiB │ 2 weeks ago │
# => │ 11 │ wix │ dir │ 0 B │ 2 weeks ago │ 11 │ wix │ dir │ 0 B │ 2 weeks ago │
# => ╰────┴──────────┴──────┴─────────┴───────────────╯ ╰────┴──────────┴──────┴─────────┴───────────────╯
``` ```
Because most of the time you'll want to see the output of a pipeline, `table` is assumed. Because most of the time you'll want to see the output of a pipeline, `table` is assumed.
We could have also written the above: We could have also written the above:
```shell ```shell
ls | where type == "dir" > ls | where type == "dir"
``` ```
Being able to use the same commands and compose them differently is an important philosophy in Nu. Being able to use the same commands and compose them differently is an important philosophy in Nu.
For example, we could use the built-in `ps` command to get a list of the running processes, using the same `where` as above. For example, we could use the built-in `ps` command to get a list of the running processes, using the same `where` as above.
```shell ```shell
ps | where cpu > 0 > ps | where cpu > 0
# => ╭───┬───────┬───────────┬───────┬───────────┬───────────╮ ╭───┬───────┬───────────┬───────┬───────────┬───────────╮
# => │ # │ pid │ name │ cpu │ mem │ virtual │ # │ pid │ name │ cpu │ mem │ virtual │
# => ├───┼───────┼───────────┼───────┼───────────┼───────────┤ ├───┼───────┼───────────┼───────┼───────────┼───────────┤
# => │ 0 │ 2240 │ Slack.exe │ 16.40 │ 178.3 MiB │ 232.6 MiB │ 02240 │ Slack.exe │ 16.40 │ 178.3 MiB │ 232.6 MiB │
# => │ 1 │ 16948 │ Slack.exe │ 16.32 │ 205.0 MiB │ 197.9 MiB │ 116948 │ Slack.exe │ 16.32 │ 205.0 MiB │ 197.9 MiB │
# => │ 2 │ 17700 │ nu.exe │ 3.77 │ 26.1 MiB │ 8.8 MiB │ 217700 │ nu.exe │ 3.77 │ 26.1 MiB │ 8.8 MiB │
# => ╰───┴───────┴───────────┴───────┴───────────┴───────────╯ ╰───┴───────┴───────────┴───────┴───────────┴───────────╯
``` ```
### Opening files ### Opening files
@ -140,46 +141,46 @@ Nu can load file and URL contents as raw text or structured data (if it recogniz
For example, you can load a .toml file as structured data and explore it: For example, you can load a .toml file as structured data and explore it:
```shell ```shell
open Cargo.toml > open Cargo.toml
# => ╭──────────────────┬────────────────────╮ ╭──────────────────┬────────────────────╮
# => │ bin │ [table 1 row] │ bin │ [table 1 row]
# => │ dependencies │ {record 25 fields} │ dependencies │ {record 25 fields}
# => │ dev-dependencies │ {record 8 fields} │ dev-dependencies │ {record 8 fields}
# => │ features │ {record 10 fields} │ features │ {record 10 fields}
# => │ package │ {record 13 fields} │ package │ {record 13 fields}
# => │ patch │ {record 1 field} │ patch │ {record 1 field}
# => │ profile │ {record 3 fields} │ profile │ {record 3 fields}
# => │ target │ {record 3 fields} │ target │ {record 3 fields}
# => │ workspace │ {record 1 field} │ workspace │ {record 1 field}
# => ╰──────────────────┴────────────────────╯ ╰──────────────────┴────────────────────╯
``` ```
We can pipe this into a command that gets the contents of one of the columns: We can pipe this into a command that gets the contents of one of the columns:
```shell ```shell
open Cargo.toml | get package > open Cargo.toml | get package
# => ╭───────────────┬────────────────────────────────────╮ ╭───────────────┬────────────────────────────────────╮
# => │ authors │ [list 1 item] │ authors │ [list 1 item]
# => │ default-run │ nu │ │ default-run │ nu │
# => │ description │ A new type of shell │ │ description │ A new type of shell │
# => │ documentation │ https://www.nushell.sh/book/ │ │ documentation │ https://www.nushell.sh/book/ │
# => │ edition │ 2018 │ │ edition │ 2018
# => │ exclude │ [list 1 item] │ exclude │ [list 1 item]
# => │ homepage │ https://www.nushell.sh │ │ homepage │ https://www.nushell.sh │
# => │ license │ MIT │ │ license │ MIT │
# => │ metadata │ {record 1 field} │ metadata │ {record 1 field}
# => │ name │ nu │ │ name │ nu │
# => │ repository │ https://github.com/nushell/nushell │ │ repository │ https://github.com/nushell/nushell │
# => │ rust-version │ 1.60 │ │ rust-version │ 1.60 │
# => │ version │ 0.72.0 │ │ version │ 0.72.0 │
# => ╰───────────────┴────────────────────────────────────╯ ╰───────────────┴────────────────────────────────────╯
``` ```
And if needed we can drill down further: And if needed we can drill down further:
```shell ```shell
open Cargo.toml | get package.version > open Cargo.toml | get package.version
# => 0.72.0 0.72.0
``` ```
### Plugins ### Plugins
@ -221,14 +222,13 @@ Please submit an issue or PR to be added to this list.
- [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell) - [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell)
- [Dorothy](http://github.com/bevry/dorothy) - [Dorothy](http://github.com/bevry/dorothy)
- [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell) - [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell)
- [x-cmd](https://x-cmd.com/mod/nu)
## Contributing ## Contributing
See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed! See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed!
<a href="https://github.com/nushell/nushell/graphs/contributors"> <a href="https://github.com/nushell/nushell/graphs/contributors">
<img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=750&columns=20" /> <img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=750" />
</a> </a>
## License ## License

View File

@ -1,29 +0,0 @@
# Security Policy
As a shell and programming language Nushell provides you with great powers and the potential to do dangerous things to your computer and data. Whenever there is a risk that a malicious actor can abuse a bug or a violation of documented behavior/assumptions in Nushell to harm you this is a *security* risk.
We want to fix those issues without exposing our users to unnecessary risk. Thus we want to explain our security policy.
Additional issues may be part of *safety* where the behavior of Nushell as designed and implemented can cause unintended harm or a bug causes damage without the involvement of a third party.
## Supported Versions
As Nushell is still under very active pre-stable development, the only version the core team prioritizes for security and safety fixes is the [most recent version as published on GitHub](https://github.com/nushell/nushell/releases/latest).
Only if you provide a strong reasoning and the necessary resources, will we consider blessing a backported fix with an official patch release for a previous version.
## Reporting a Vulnerability
If you suspect that a bug or behavior of Nushell can affect security or may be potentially exploitable, please report the issue to us in private.
Either reach out to the core team on [our Discord server](https://discord.gg/NtAbbGn) to arrange a private channel or use the [GitHub vulnerability reporting form](https://github.com/nushell/nushell/security/advisories/new).
Please try to answer the following questions:
- How can we reach you for further questions?
- What is the bug? Which system of Nushell may be affected?
- Do you have proof-of-concept for a potential exploit or have you observed an exploit in the wild?
- What is your assessment of the severity based on what could be impacted should the bug be exploited?
- Are additional people aware of the issue or deserve credit for identifying the issue?
We will try to get back to you within a week with:
- acknowledging the receipt of the report
- an initial plan of how we want to address this including the primary points of contact for further communication
- our preliminary assessment of how severe we judge the issue
- a proposal for how we can coordinate responsible disclosure (e.g. how we ship the bugfix, if we need to coordinate with distribution maintainers, when you can release a blog post if you want to etc.)
For purely *safety* related issues where the impact is severe by direct user action instead of malicious input or third parties, feel free to open a regular issue. If we deem that there may be an additional *security* risk on a *safety* issue we may continue discussions in a restricted forum.

View File

@ -1,40 +1,95 @@
use nu_cli::{eval_source, evaluate_commands}; use nu_cli::{eval_source, evaluate_commands};
use nu_plugin_core::{Encoder, EncodingType}; use nu_parser::parse;
use nu_plugin_protocol::{PluginCallResponse, PluginOutput}; use nu_plugin::{Encoder, EncodingType, PluginCallResponse, PluginOutput};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PipelineData, Signals, Span, Spanned, Value, eval_const::create_nu_constant,
PipelineData, Span, Spanned, Value, NU_VARIABLE_ID,
}; };
use nu_std::load_standard_library; use nu_std::load_standard_library;
use nu_utils::{get_default_config, get_default_env}; use nu_utils::{get_default_config, get_default_env};
use std::{ use std::path::{Path, PathBuf};
fmt::Write,
hint::black_box, fn main() {
rc::Rc, // Run registered benchmarks.
sync::{atomic::AtomicBool, Arc}, divan::main();
}; }
use tango_bench::{benchmark_fn, tango_benchmarks, tango_main, IntoBenchmarks};
fn load_bench_commands() -> EngineState { fn load_bench_commands() -> EngineState {
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()) nu_command::add_shell_command_context(nu_cmd_lang::create_default_context())
} }
fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf {
let cwd = engine_state.current_work_dir();
if path.exists() {
match nu_path::canonicalize_with(path, cwd) {
Ok(canon_path) => canon_path,
Err(_) => path.to_owned(),
}
} else {
path.to_owned()
}
}
fn get_home_path(engine_state: &EngineState) -> PathBuf {
nu_path::home_dir()
.map(|path| canonicalize_path(engine_state, &path))
.unwrap_or_default()
}
fn setup_engine() -> EngineState { fn setup_engine() -> EngineState {
let mut engine_state = load_bench_commands(); let mut engine_state = load_bench_commands();
let cwd = std::env::current_dir() let home_path = get_home_path(&engine_state);
.unwrap()
.into_os_string()
.into_string()
.unwrap();
// parsing config.nu breaks without PWD set, so set a valid path // parsing config.nu breaks without PWD set, so set a valid path
engine_state.add_env_var("PWD".into(), Value::string(cwd, Span::test_data())); engine_state.add_env_var(
"PWD".into(),
Value::string(home_path.to_string_lossy(), Span::test_data()),
);
engine_state.generate_nu_constant(); let nu_const = create_nu_constant(&engine_state, Span::unknown())
.expect("Failed to create nushell constant.");
engine_state.set_variable_const_val(NU_VARIABLE_ID, nu_const);
engine_state engine_state
} }
fn bench_command(bencher: divan::Bencher, scaled_command: String) {
bench_command_with_custom_stack_and_engine(
bencher,
scaled_command,
Stack::new(),
setup_engine(),
)
}
fn bench_command_with_custom_stack_and_engine(
bencher: divan::Bencher,
scaled_command: String,
stack: nu_protocol::engine::Stack,
mut engine: EngineState,
) {
load_standard_library(&mut engine).unwrap();
let commands = Spanned {
span: Span::unknown(),
item: scaled_command,
};
bencher
.with_inputs(|| engine.clone())
.bench_values(|mut engine| {
evaluate_commands(
&commands,
&mut engine,
&mut stack.clone(),
PipelineData::empty(),
None,
)
.unwrap();
})
}
fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) { fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
let mut engine = setup_engine(); let mut engine = setup_engine();
let commands = Spanned { let commands = Spanned {
@ -43,19 +98,272 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
}; };
let mut stack = Stack::new(); let mut stack = Stack::new();
evaluate_commands( evaluate_commands(
&commands, &commands,
&mut engine, &mut engine,
&mut stack, &mut stack,
PipelineData::empty(), PipelineData::empty(),
Default::default(), None,
) )
.unwrap(); .unwrap();
(stack, engine) (stack, engine)
} }
// FIXME: All benchmarks live in this 1 file to speed up build times when benchmarking.
// When the *_benchmarks functions were in different files, `cargo bench` would build
// an executable for every single one - incredibly slowly. Would be nice to figure out
// a way to split things up again.
#[divan::bench]
fn load_standard_lib(bencher: divan::Bencher) {
let engine = setup_engine();
bencher
.with_inputs(|| engine.clone())
.bench_values(|mut engine| {
load_standard_library(&mut engine).unwrap();
})
}
#[divan::bench_group]
mod record {
use super::*;
fn create_flat_record_string(n: i32) -> String {
let mut s = String::from("let record = {");
for i in 0..n {
s.push_str(&format!("col_{}: {}", i, i));
if i < n - 1 {
s.push_str(", ");
}
}
s.push('}');
s
}
fn create_nested_record_string(depth: i32) -> String {
let mut s = String::from("let record = {");
for _ in 0..depth {
s.push_str("col: {");
}
s.push_str("col_final: 0");
for _ in 0..depth {
s.push('}');
}
s.push('}');
s
}
#[divan::bench(args = [1, 10, 100, 1000])]
fn create(bencher: divan::Bencher, n: i32) {
bench_command(bencher, create_flat_record_string(n));
}
#[divan::bench(args = [1, 10, 100, 1000])]
fn flat_access(bencher: divan::Bencher, n: i32) {
let (stack, engine) = setup_stack_and_engine_from_command(&create_flat_record_string(n));
bench_command_with_custom_stack_and_engine(
bencher,
"$record.col_0 | ignore".to_string(),
stack,
engine,
);
}
#[divan::bench(args = [1, 2, 4, 8, 16, 32, 64, 128])]
fn nest_access(bencher: divan::Bencher, depth: i32) {
let (stack, engine) =
setup_stack_and_engine_from_command(&create_nested_record_string(depth));
let nested_access = ".col".repeat(depth as usize);
bench_command_with_custom_stack_and_engine(
bencher,
format!("$record{} | ignore", nested_access),
stack,
engine,
);
}
}
#[divan::bench_group]
mod table {
use super::*;
fn create_example_table_nrows(n: i32) -> String {
let mut s = String::from("let table = [[foo bar baz]; ");
for i in 0..n {
s.push_str(&format!("[0, 1, {i}]"));
if i < n - 1 {
s.push_str(", ");
}
}
s.push(']');
s
}
#[divan::bench(args = [1, 10, 100, 1000])]
fn create(bencher: divan::Bencher, n: i32) {
bench_command(bencher, create_example_table_nrows(n));
}
#[divan::bench(args = [1, 10, 100, 1000])]
fn get(bencher: divan::Bencher, n: i32) {
let (stack, engine) = setup_stack_and_engine_from_command(&create_example_table_nrows(n));
bench_command_with_custom_stack_and_engine(
bencher,
"$table | get bar | math sum | ignore".to_string(),
stack,
engine,
);
}
#[divan::bench(args = [1, 10, 100, 1000])]
fn select(bencher: divan::Bencher, n: i32) {
let (stack, engine) = setup_stack_and_engine_from_command(&create_example_table_nrows(n));
bench_command_with_custom_stack_and_engine(
bencher,
"$table | select foo baz | ignore".to_string(),
stack,
engine,
);
}
}
#[divan::bench_group]
mod eval_commands {
use super::*;
#[divan::bench(args = [100, 1_000, 10_000])]
fn interleave(bencher: divan::Bencher, n: i32) {
bench_command(
bencher,
format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"),
)
}
#[divan::bench(args = [100, 1_000, 10_000])]
fn interleave_with_ctrlc(bencher: divan::Bencher, n: i32) {
let mut engine = setup_engine();
engine.ctrlc = Some(std::sync::Arc::new(std::sync::atomic::AtomicBool::new(
false,
)));
load_standard_library(&mut engine).unwrap();
let commands = Spanned {
span: Span::unknown(),
item: format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"),
};
bencher
.with_inputs(|| engine.clone())
.bench_values(|mut engine| {
evaluate_commands(
&commands,
&mut engine,
&mut nu_protocol::engine::Stack::new(),
PipelineData::empty(),
None,
)
.unwrap();
})
}
#[divan::bench(args = [1, 5, 10, 100, 1_000])]
fn for_range(bencher: divan::Bencher, n: i32) {
bench_command(bencher, format!("(for $x in (1..{}) {{ sleep 50ns }})", n))
}
#[divan::bench(args = [1, 5, 10, 100, 1_000])]
fn each(bencher: divan::Bencher, n: i32) {
bench_command(
bencher,
format!("(1..{}) | each {{|_| sleep 50ns }} | ignore", n),
)
}
#[divan::bench(args = [1, 5, 10, 100, 1_000])]
fn par_each_1t(bencher: divan::Bencher, n: i32) {
bench_command(
bencher,
format!("(1..{}) | par-each -t 1 {{|_| sleep 50ns }} | ignore", n),
)
}
#[divan::bench(args = [1, 5, 10, 100, 1_000])]
fn par_each_2t(bencher: divan::Bencher, n: i32) {
bench_command(
bencher,
format!("(1..{}) | par-each -t 2 {{|_| sleep 50ns }} | ignore", n),
)
}
}
#[divan::bench_group()]
mod parser_benchmarks {
use super::*;
#[divan::bench()]
fn parse_default_config_file(bencher: divan::Bencher) {
let engine_state = setup_engine();
let default_env = get_default_config().as_bytes();
bencher
.with_inputs(|| nu_protocol::engine::StateWorkingSet::new(&engine_state))
.bench_refs(|working_set| parse(working_set, None, default_env, false))
}
#[divan::bench()]
fn parse_default_env_file(bencher: divan::Bencher) {
let engine_state = setup_engine();
let default_env = get_default_env().as_bytes();
bencher
.with_inputs(|| nu_protocol::engine::StateWorkingSet::new(&engine_state))
.bench_refs(|working_set| parse(working_set, None, default_env, false))
}
}
#[divan::bench_group()]
mod eval_benchmarks {
use super::*;
#[divan::bench()]
fn eval_default_env(bencher: divan::Bencher) {
let default_env = get_default_env().as_bytes();
let fname = "default_env.nu";
bencher
.with_inputs(|| (setup_engine(), nu_protocol::engine::Stack::new()))
.bench_values(|(mut engine_state, mut stack)| {
eval_source(
&mut engine_state,
&mut stack,
default_env,
fname,
PipelineData::empty(),
false,
)
})
}
#[divan::bench()]
fn eval_default_config(bencher: divan::Bencher) {
let default_env = get_default_config().as_bytes();
let fname = "default_config.nu";
bencher
.with_inputs(|| (setup_engine(), nu_protocol::engine::Stack::new()))
.bench_values(|(mut engine_state, mut stack)| {
eval_source(
&mut engine_state,
&mut stack,
default_env,
fname,
PipelineData::empty(),
false,
)
})
}
}
// generate a new table data with `row_cnt` rows, `col_cnt` columns. // generate a new table data with `row_cnt` rows, `col_cnt` columns.
fn encoding_test_data(row_cnt: usize, col_cnt: usize) -> Value { fn encoding_test_data(row_cnt: usize, col_cnt: usize) -> Value {
let record = Value::test_record( let record = Value::test_record(
@ -67,475 +375,76 @@ fn encoding_test_data(row_cnt: usize, col_cnt: usize) -> Value {
Value::list(vec![record; row_cnt], Span::test_data()) Value::list(vec![record; row_cnt], Span::test_data())
} }
fn bench_command( #[divan::bench_group()]
name: &str, mod encoding_benchmarks {
command: &str, use super::*;
stack: Stack,
engine: EngineState,
) -> impl IntoBenchmarks {
let commands = Spanned {
span: Span::unknown(),
item: command.to_string(),
};
[benchmark_fn(name, move |b| {
let commands = commands.clone();
let stack = stack.clone();
let engine = engine.clone();
b.iter(move || {
let mut stack = stack.clone();
let mut engine = engine.clone();
#[allow(clippy::unit_arg)]
black_box(
evaluate_commands(
&commands,
&mut engine,
&mut stack,
PipelineData::empty(),
Default::default(),
)
.unwrap(),
);
})
})]
}
fn bench_eval_source( #[divan::bench(args = [(100, 5), (10000, 15)])]
name: &str, fn json_encode(bencher: divan::Bencher, (row_cnt, col_cnt): (usize, usize)) {
fname: String, let test_data = PluginOutput::CallResponse(
source: Vec<u8>, 0,
stack: Stack, PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
engine: EngineState, );
) -> impl IntoBenchmarks { let encoder = EncodingType::try_from_bytes(b"json").unwrap();
[benchmark_fn(name, move |b| { bencher
let stack = stack.clone(); .with_inputs(Vec::new)
let engine = engine.clone(); .bench_values(|mut res| encoder.encode(&test_data, &mut res))
let fname = fname.clone();
let source = source.clone();
b.iter(move || {
let mut stack = stack.clone();
let mut engine = engine.clone();
let fname: &str = &fname.clone();
let source: &[u8] = &source.clone();
black_box(eval_source(
&mut engine,
&mut stack,
source,
fname,
PipelineData::empty(),
false,
));
})
})]
}
/// Load the standard library into the engine.
fn bench_load_standard_lib() -> impl IntoBenchmarks {
[benchmark_fn("load_standard_lib", move |b| {
let engine = setup_engine();
b.iter(move || {
let mut engine = engine.clone();
load_standard_library(&mut engine)
})
})]
}
fn create_flat_record_string(n: usize) -> String {
let mut s = String::from("let record = { ");
for i in 0..n {
write!(s, "col_{i}: {i}, ").unwrap();
} }
s.push('}');
s
}
fn create_nested_record_string(depth: usize) -> String { #[divan::bench(args = [(100, 5), (10000, 15)])]
let mut s = String::from("let record = {"); fn msgpack_encode(bencher: divan::Bencher, (row_cnt, col_cnt): (usize, usize)) {
for _ in 0..depth { let test_data = PluginOutput::CallResponse(
s.push_str("col: {"); 0,
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
);
let encoder = EncodingType::try_from_bytes(b"msgpack").unwrap();
bencher
.with_inputs(Vec::new)
.bench_values(|mut res| encoder.encode(&test_data, &mut res))
} }
s.push_str("col_final: 0");
for _ in 0..depth {
s.push('}');
}
s.push('}');
s
} }
fn create_example_table_nrows(n: usize) -> String { #[divan::bench_group()]
let mut s = String::from("let table = [[foo bar baz]; "); mod decoding_benchmarks {
for i in 0..n { use super::*;
s.push_str(&format!("[0, 1, {i}]"));
if i < n - 1 {
s.push_str(", ");
}
}
s.push(']');
s
}
fn bench_record_create(n: usize) -> impl IntoBenchmarks { #[divan::bench(args = [(100, 5), (10000, 15)])]
bench_command( fn json_decode(bencher: divan::Bencher, (row_cnt, col_cnt): (usize, usize)) {
&format!("record_create_{n}"), let test_data = PluginOutput::CallResponse(
&create_flat_record_string(n), 0,
Stack::new(), PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
setup_engine(), );
) let encoder = EncodingType::try_from_bytes(b"json").unwrap();
} let mut res = vec![];
encoder.encode(&test_data, &mut res).unwrap();
fn bench_record_flat_access(n: usize) -> impl IntoBenchmarks { bencher
let setup_command = create_flat_record_string(n); .with_inputs(|| {
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
bench_command(
&format!("record_flat_access_{n}"),
"$record.col_0 | ignore",
stack,
engine,
)
}
fn bench_record_nested_access(n: usize) -> impl IntoBenchmarks {
let setup_command = create_nested_record_string(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
let nested_access = ".col".repeat(n);
bench_command(
&format!("record_nested_access_{n}"),
&format!("$record{} | ignore", nested_access),
stack,
engine,
)
}
fn bench_record_insert(n: usize, m: usize) -> impl IntoBenchmarks {
let setup_command = create_flat_record_string(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
let mut insert = String::from("$record");
for i in n..(n + m) {
write!(insert, " | insert col_{i} {i}").unwrap();
}
insert.push_str(" | ignore");
bench_command(&format!("record_insert_{n}_{m}"), &insert, stack, engine)
}
fn bench_table_create(n: usize) -> impl IntoBenchmarks {
bench_command(
&format!("table_create_{n}"),
&create_example_table_nrows(n),
Stack::new(),
setup_engine(),
)
}
fn bench_table_get(n: usize) -> impl IntoBenchmarks {
let setup_command = create_example_table_nrows(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
bench_command(
&format!("table_get_{n}"),
"$table | get bar | math sum | ignore",
stack,
engine,
)
}
fn bench_table_select(n: usize) -> impl IntoBenchmarks {
let setup_command = create_example_table_nrows(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
bench_command(
&format!("table_select_{n}"),
"$table | select foo baz | ignore",
stack,
engine,
)
}
fn bench_table_insert_row(n: usize, m: usize) -> impl IntoBenchmarks {
let setup_command = create_example_table_nrows(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
let mut insert = String::from("$table");
for i in n..(n + m) {
write!(insert, " | insert {i} {{ foo: 0, bar: 1, baz: {i} }}").unwrap();
}
insert.push_str(" | ignore");
bench_command(&format!("table_insert_row_{n}_{m}"), &insert, stack, engine)
}
fn bench_table_insert_col(n: usize, m: usize) -> impl IntoBenchmarks {
let setup_command = create_example_table_nrows(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
let mut insert = String::from("$table");
for i in 0..m {
write!(insert, " | insert col_{i} {i}").unwrap();
}
insert.push_str(" | ignore");
bench_command(&format!("table_insert_col_{n}_{m}"), &insert, stack, engine)
}
fn bench_eval_interleave(n: usize) -> impl IntoBenchmarks {
let engine = setup_engine();
let stack = Stack::new();
bench_command(
&format!("eval_interleave_{n}"),
&format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"),
stack,
engine,
)
}
fn bench_eval_interleave_with_interrupt(n: usize) -> impl IntoBenchmarks {
let mut engine = setup_engine();
engine.set_signals(Signals::new(Arc::new(AtomicBool::new(false))));
let stack = Stack::new();
bench_command(
&format!("eval_interleave_with_interrupt_{n}"),
&format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"),
stack,
engine,
)
}
fn bench_eval_for(n: usize) -> impl IntoBenchmarks {
let engine = setup_engine();
let stack = Stack::new();
bench_command(
&format!("eval_for_{n}"),
&format!("(for $x in (1..{n}) {{ 1 }}) | ignore"),
stack,
engine,
)
}
fn bench_eval_each(n: usize) -> impl IntoBenchmarks {
let engine = setup_engine();
let stack = Stack::new();
bench_command(
&format!("eval_each_{n}"),
&format!("(1..{n}) | each {{|_| 1 }} | ignore"),
stack,
engine,
)
}
fn bench_eval_par_each(n: usize) -> impl IntoBenchmarks {
let engine = setup_engine();
let stack = Stack::new();
bench_command(
&format!("eval_par_each_{n}"),
&format!("(1..{}) | par-each -t 2 {{|_| 1 }} | ignore", n),
stack,
engine,
)
}
fn bench_eval_default_config() -> impl IntoBenchmarks {
let default_env = get_default_config().as_bytes().to_vec();
let fname = "default_config.nu".to_string();
bench_eval_source(
"eval_default_config",
fname,
default_env,
Stack::new(),
setup_engine(),
)
}
fn bench_eval_default_env() -> impl IntoBenchmarks {
let default_env = get_default_env().as_bytes().to_vec();
let fname = "default_env.nu".to_string();
bench_eval_source(
"eval_default_env",
fname,
default_env,
Stack::new(),
setup_engine(),
)
}
fn encode_json(row_cnt: usize, col_cnt: usize) -> impl IntoBenchmarks {
let test_data = Rc::new(PluginOutput::CallResponse(
0,
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
));
let encoder = Rc::new(EncodingType::try_from_bytes(b"json").unwrap());
[benchmark_fn(
format!("encode_json_{}_{}", row_cnt, col_cnt),
move |b| {
let encoder = encoder.clone();
let test_data = test_data.clone();
b.iter(move || {
let mut res = Vec::new();
encoder.encode(&*test_data, &mut res).unwrap();
})
},
)]
}
fn encode_msgpack(row_cnt: usize, col_cnt: usize) -> impl IntoBenchmarks {
let test_data = Rc::new(PluginOutput::CallResponse(
0,
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
));
let encoder = Rc::new(EncodingType::try_from_bytes(b"msgpack").unwrap());
[benchmark_fn(
format!("encode_msgpack_{}_{}", row_cnt, col_cnt),
move |b| {
let encoder = encoder.clone();
let test_data = test_data.clone();
b.iter(move || {
let mut res = Vec::new();
encoder.encode(&*test_data, &mut res).unwrap();
})
},
)]
}
fn decode_json(row_cnt: usize, col_cnt: usize) -> impl IntoBenchmarks {
let test_data = PluginOutput::CallResponse(
0,
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
);
let encoder = EncodingType::try_from_bytes(b"json").unwrap();
let mut res = vec![];
encoder.encode(&test_data, &mut res).unwrap();
[benchmark_fn(
format!("decode_json_{}_{}", row_cnt, col_cnt),
move |b| {
let res = res.clone();
b.iter(move || {
let mut binary_data = std::io::Cursor::new(res.clone()); let mut binary_data = std::io::Cursor::new(res.clone());
binary_data.set_position(0); binary_data.set_position(0);
let _: Result<Option<PluginOutput>, _> = binary_data
black_box(encoder.decode(&mut binary_data));
}) })
}, .bench_values(|mut binary_data| -> Result<Option<PluginOutput>, _> {
)] encoder.decode(&mut binary_data)
} })
}
fn decode_msgpack(row_cnt: usize, col_cnt: usize) -> impl IntoBenchmarks { #[divan::bench(args = [(100, 5), (10000, 15)])]
let test_data = PluginOutput::CallResponse( fn msgpack_decode(bencher: divan::Bencher, (row_cnt, col_cnt): (usize, usize)) {
0, let test_data = PluginOutput::CallResponse(
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)), 0,
); PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
let encoder = EncodingType::try_from_bytes(b"msgpack").unwrap(); );
let mut res = vec![]; let encoder = EncodingType::try_from_bytes(b"msgpack").unwrap();
encoder.encode(&test_data, &mut res).unwrap(); let mut res = vec![];
encoder.encode(&test_data, &mut res).unwrap();
[benchmark_fn( bencher
format!("decode_msgpack_{}_{}", row_cnt, col_cnt), .with_inputs(|| {
move |b| {
let res = res.clone();
b.iter(move || {
let mut binary_data = std::io::Cursor::new(res.clone()); let mut binary_data = std::io::Cursor::new(res.clone());
binary_data.set_position(0); binary_data.set_position(0);
let _: Result<Option<PluginOutput>, _> = binary_data
black_box(encoder.decode(&mut binary_data));
}) })
}, .bench_values(|mut binary_data| -> Result<Option<PluginOutput>, _> {
)] encoder.decode(&mut binary_data)
})
}
} }
tango_benchmarks!(
bench_load_standard_lib(),
// Data types
// Record
bench_record_create(1),
bench_record_create(10),
bench_record_create(100),
bench_record_create(1_000),
bench_record_flat_access(1),
bench_record_flat_access(10),
bench_record_flat_access(100),
bench_record_flat_access(1_000),
bench_record_nested_access(1),
bench_record_nested_access(2),
bench_record_nested_access(4),
bench_record_nested_access(8),
bench_record_nested_access(16),
bench_record_nested_access(32),
bench_record_nested_access(64),
bench_record_nested_access(128),
bench_record_insert(1, 1),
bench_record_insert(10, 1),
bench_record_insert(100, 1),
bench_record_insert(1000, 1),
bench_record_insert(1, 10),
bench_record_insert(10, 10),
bench_record_insert(100, 10),
bench_record_insert(1000, 10),
// Table
bench_table_create(1),
bench_table_create(10),
bench_table_create(100),
bench_table_create(1_000),
bench_table_get(1),
bench_table_get(10),
bench_table_get(100),
bench_table_get(1_000),
bench_table_select(1),
bench_table_select(10),
bench_table_select(100),
bench_table_select(1_000),
bench_table_insert_row(1, 1),
bench_table_insert_row(10, 1),
bench_table_insert_row(100, 1),
bench_table_insert_row(1000, 1),
bench_table_insert_row(1, 10),
bench_table_insert_row(10, 10),
bench_table_insert_row(100, 10),
bench_table_insert_row(1000, 10),
bench_table_insert_col(1, 1),
bench_table_insert_col(10, 1),
bench_table_insert_col(100, 1),
bench_table_insert_col(1000, 1),
bench_table_insert_col(1, 10),
bench_table_insert_col(10, 10),
bench_table_insert_col(100, 10),
bench_table_insert_col(1000, 10),
// Eval
// Interleave
bench_eval_interleave(100),
bench_eval_interleave(1_000),
bench_eval_interleave(10_000),
bench_eval_interleave_with_interrupt(100),
bench_eval_interleave_with_interrupt(1_000),
bench_eval_interleave_with_interrupt(10_000),
// For
bench_eval_for(1),
bench_eval_for(10),
bench_eval_for(100),
bench_eval_for(1_000),
bench_eval_for(10_000),
// Each
bench_eval_each(1),
bench_eval_each(10),
bench_eval_each(100),
bench_eval_each(1_000),
bench_eval_each(10_000),
// Par-Each
bench_eval_par_each(1),
bench_eval_par_each(10),
bench_eval_par_each(100),
bench_eval_par_each(1_000),
bench_eval_par_each(10_000),
// Config
bench_eval_default_config(),
// Env
bench_eval_default_env(),
// Encode
// Json
encode_json(100, 5),
encode_json(10000, 15),
// MsgPack
encode_msgpack(100, 5),
encode_msgpack(10000, 15),
// Decode
// Json
decode_json(100, 5),
decode_json(10000, 15),
// MsgPack
decode_msgpack(100, 5),
decode_msgpack(10000, 15)
);
tango_main!();

View File

@ -1,3 +0,0 @@
[[disallowed-types]]
path = "std::time::Instant"
reason = "WASM panics if used, use `web_time::Instant` instead"

View File

@ -5,50 +5,44 @@ 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.104.1" version = "0.92.2"
[lib] [lib]
bench = false bench = false
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.104.1" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.92.2" }
nu-command = { path = "../nu-command", version = "0.104.1" } nu-command = { path = "../nu-command", version = "0.92.2" }
nu-std = { path = "../nu-std", version = "0.104.1" } nu-test-support = { path = "../nu-test-support", version = "0.92.2" }
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 }
[dependencies] [dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.1" } nu-cmd-base = { path = "../nu-cmd-base", version = "0.92.2" }
nu-engine = { path = "../nu-engine", version = "0.104.1", features = ["os"] } nu-engine = { path = "../nu-engine", version = "0.92.2" }
nu-glob = { path = "../nu-glob", version = "0.104.1" } nu-path = { path = "../nu-path", version = "0.92.2" }
nu-path = { path = "../nu-path", version = "0.104.1" } nu-parser = { path = "../nu-parser", version = "0.92.2" }
nu-parser = { path = "../nu-parser", version = "0.104.1" } nu-protocol = { path = "../nu-protocol", version = "0.92.2" }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.104.1", optional = true } nu-utils = { path = "../nu-utils", version = "0.92.2" }
nu-protocol = { path = "../nu-protocol", version = "0.104.1", features = ["os"] } nu-color-config = { path = "../nu-color-config", version = "0.92.2" }
nu-utils = { path = "../nu-utils", version = "0.104.1" }
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"] }
chrono = { default-features = false, features = ["std"], workspace = true } chrono = { default-features = false, features = ["std"], workspace = true }
crossterm = { workspace = true } crossterm = { workspace = true }
fancy-regex = { workspace = true } fancy-regex = { workspace = true }
fuzzy-matcher = { workspace = true }
is_executable = { workspace = true } is_executable = { workspace = true }
log = { workspace = true } log = { workspace = true }
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
miette = { workspace = true, features = ["fancy-no-backtrace"] } miette = { workspace = true, features = ["fancy-no-backtrace"] }
nucleo-matcher = { workspace = true } lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
once_cell = { workspace = true }
percent-encoding = { workspace = true } percent-encoding = { workspace = true }
pathdiff = { workspace = true }
sysinfo = { workspace = true } sysinfo = { workspace = true }
strum = { workspace = true }
unicode-segmentation = { workspace = true } unicode-segmentation = { workspace = true }
uuid = { workspace = true, features = ["v4"] } uuid = { workspace = true, features = ["v4"] }
which = { workspace = true } which = { workspace = true }
[features] [features]
plugin = ["nu-plugin-engine"] plugin = []
system-clipboard = ["reedline/system_clipboard"] system-clipboard = ["reedline/system_clipboard"]
[lints]
workspace = true

View File

@ -1,7 +0,0 @@
This crate implements the core functionality of the interactive Nushell REPL and interfaces with `reedline`.
Currently implements the syntax highlighting and completions logic.
Furthermore includes a few commands that are specific to `reedline`
## Internal Nushell crate
This crate implements components of Nushell and is not designed to support plugin authors or other users directly.

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)] #[derive(Clone)]
pub struct Commandline; pub struct Commandline;
@ -10,12 +11,45 @@ impl Command for Commandline {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("commandline") Signature::build("commandline")
.input_output_types(vec![(Type::Nothing, Type::String)]) .input_output_types(vec![
(Type::Nothing, Type::Nothing),
(Type::String, Type::String),
])
.switch(
"cursor",
"Set or get the current cursor position",
Some('c'),
)
.switch(
"cursor-end",
"Set the current cursor position to the end of the buffer",
Some('e'),
)
.switch(
"append",
"appends the string to the end of the buffer",
Some('a'),
)
.switch(
"insert",
"inserts the string into the buffer at the cursor position",
Some('i'),
)
.switch(
"replace",
"replaces the current contents of the buffer (default)",
Some('r'),
)
.optional(
"cmd",
SyntaxShape::String,
"the string to perform the operation with",
)
.category(Category::Core) .category(Category::Core)
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"View the current command line input buffer." "View or modify the current command line input buffer."
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
@ -25,11 +59,126 @@ impl Command for Commandline {
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
_stack: &mut Stack, stack: &mut Stack,
call: &Call, call: &Call,
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let repl = engine_state.repl_state.lock().expect("repl state mutex"); if let Some(cmd) = call.opt::<Value>(engine_state, stack, 0)? {
Ok(Value::string(repl.buffer.clone(), call.head).into_pipeline_data()) let span = cmd.span();
let cmd = cmd.coerce_into_string()?;
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
if call.has_flag(engine_state, stack, "cursor")? {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "`--cursor (-c)` is deprecated".into(),
msg: "Setting the current cursor position by `--cursor (-c)` is deprecated"
.into(),
span: Some(call.arguments_span()),
help: Some("Use `commandline set-cursor`".into()),
inner: vec![],
},
);
match cmd.parse::<i64>() {
Ok(n) => {
repl.cursor_pos = if n <= 0 {
0usize
} else {
repl.buffer
.grapheme_indices(true)
.map(|(i, _c)| i)
.nth(n as usize)
.unwrap_or(repl.buffer.len())
}
}
Err(_) => {
return Err(ShellError::CantConvert {
to_type: "int".to_string(),
from_type: "string".to_string(),
span,
help: Some(format!(r#"string "{cmd}" does not represent a valid int"#)),
})
}
}
} else if call.has_flag(engine_state, stack, "append")? {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "`--append (-a)` is deprecated".into(),
msg: "Appending the string to the end of the buffer by `--append (-a)` is deprecated".into(),
span: Some(call.arguments_span()),
help: Some("Use `commandline edit --append (-a)`".into()),
inner: vec![],
},
);
repl.buffer.push_str(&cmd);
} else if call.has_flag(engine_state, stack, "insert")? {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "`--insert (-i)` is deprecated".into(),
msg: "Inserts the string into the buffer at the cursor position by `--insert (-i)` is deprecated".into(),
span: Some(call.arguments_span()),
help: Some("Use `commandline edit --insert (-i)`".into()),
inner: vec![],
},
);
let cursor_pos = repl.cursor_pos;
repl.buffer.insert_str(cursor_pos, &cmd);
repl.cursor_pos += cmd.len();
} else {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "`--replace (-r)` is deprecated".into(),
msg: "Replacing the current contents of the buffer by `--replace (-p)` or positional argument is deprecated".into(),
span: Some(call.arguments_span()),
help: Some("Use `commandline edit --replace (-r)`".into()),
inner: vec![],
},
);
repl.buffer = cmd;
repl.cursor_pos = repl.buffer.len();
}
Ok(Value::nothing(call.head).into_pipeline_data())
} else {
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
if call.has_flag(engine_state, stack, "cursor-end")? {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "`--cursor-end (-e)` is deprecated".into(),
msg: "Setting the current cursor position to the end of the buffer by `--cursor-end (-e)` is deprecated".into(),
span: Some(call.arguments_span()),
help: Some("Use `commandline set-cursor --end (-e)`".into()),
inner: vec![],
},
);
repl.cursor_pos = repl.buffer.len();
Ok(Value::nothing(call.head).into_pipeline_data())
} else if call.has_flag(engine_state, stack, "cursor")? {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "`--cursor (-c)` is deprecated".into(),
msg: "Getting the current cursor position by `--cursor (-c)` is deprecated"
.into(),
span: Some(call.arguments_span()),
help: Some("Use `commandline get-cursor`".into()),
inner: vec![],
},
);
let char_pos = repl
.buffer
.grapheme_indices(true)
.chain(std::iter::once((repl.buffer.len(), "")))
.position(|(i, _c)| i == repl.cursor_pos)
.expect("Cursor position isn't on a grapheme boundary");
Ok(Value::string(char_pos.to_string(), call.head).into_pipeline_data())
} else {
Ok(Value::string(repl.buffer.to_string(), call.head).into_pipeline_data())
}
}
} }
} }

View File

@ -1,9 +1,9 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
#[derive(Clone)] #[derive(Clone)]
pub struct CommandlineEdit; pub struct SubCommand;
impl Command for CommandlineEdit { impl Command for SubCommand {
fn name(&self) -> &str { fn name(&self) -> &str {
"commandline edit" "commandline edit"
} }
@ -29,12 +29,12 @@ impl Command for CommandlineEdit {
.required( .required(
"str", "str",
SyntaxShape::String, SyntaxShape::String,
"The string to perform the operation with.", "the string to perform the operation with",
) )
.category(Category::Core) .category(Category::Core)
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Modify the current command line input buffer." "Modify the current command line input buffer."
} }

View File

@ -2,9 +2,9 @@ use nu_engine::command_prelude::*;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)] #[derive(Clone)]
pub struct CommandlineGetCursor; pub struct SubCommand;
impl Command for CommandlineGetCursor { impl Command for SubCommand {
fn name(&self) -> &str { fn name(&self) -> &str {
"commandline get-cursor" "commandline get-cursor"
} }
@ -16,7 +16,7 @@ impl Command for CommandlineGetCursor {
.category(Category::Core) .category(Category::Core)
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Get the current cursor position." "Get the current cursor position."
} }

View File

@ -4,6 +4,6 @@ mod get_cursor;
mod set_cursor; mod set_cursor;
pub use commandline_::Commandline; pub use commandline_::Commandline;
pub use edit::CommandlineEdit; pub use edit::SubCommand as CommandlineEdit;
pub use get_cursor::CommandlineGetCursor; pub use get_cursor::SubCommand as CommandlineGetCursor;
pub use set_cursor::CommandlineSetCursor; pub use set_cursor::SubCommand as CommandlineSetCursor;

View File

@ -3,9 +3,9 @@ use nu_engine::command_prelude::*;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)] #[derive(Clone)]
pub struct CommandlineSetCursor; pub struct SubCommand;
impl Command for CommandlineSetCursor { impl Command for SubCommand {
fn name(&self) -> &str { fn name(&self) -> &str {
"commandline set-cursor" "commandline set-cursor"
} }
@ -18,11 +18,11 @@ impl Command for CommandlineSetCursor {
"set the current cursor position to the end of the buffer", "set the current cursor position to the end of the buffer",
Some('e'), Some('e'),
) )
.optional("pos", SyntaxShape::Int, "Cursor position to be set.") .optional("pos", SyntaxShape::Int, "Cursor position to be set")
.category(Category::Core) .category(Category::Core)
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Set the current cursor position." "Set the current cursor position."
} }

View File

@ -17,7 +17,6 @@ pub fn add_cli_context(mut engine_state: EngineState) -> EngineState {
CommandlineGetCursor, CommandlineGetCursor,
CommandlineSetCursor, CommandlineSetCursor,
History, History,
HistoryImport,
HistorySession, HistorySession,
Keybindings, Keybindings,
KeybindingsDefault, KeybindingsDefault,

View File

@ -1,9 +0,0 @@
// Each const is named after a HistoryItem field, and the value is the field name to be displayed to
// the user (or accept during import).
pub const COMMAND_LINE: &str = "command";
pub const START_TIMESTAMP: &str = "start_timestamp";
pub const HOSTNAME: &str = "hostname";
pub const CWD: &str = "cwd";
pub const EXIT_STATUS: &str = "exit_status";
pub const DURATION: &str = "duration";
pub const SESSION_ID: &str = "session_id";

View File

@ -1,12 +1,10 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::{shell_error::io::IoError, HistoryFileFormat}; use nu_protocol::HistoryFileFormat;
use reedline::{ use reedline::{
FileBackedHistory, History as ReedlineHistory, HistoryItem, SearchDirection, SearchQuery, FileBackedHistory, History as ReedlineHistory, HistoryItem, SearchDirection, SearchQuery,
SqliteBackedHistory, SqliteBackedHistory,
}; };
use super::fields;
#[derive(Clone)] #[derive(Clone)]
pub struct History; pub struct History;
@ -15,7 +13,7 @@ impl Command for History {
"history" "history"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Get the command history." "Get the command history."
} }
@ -44,77 +42,91 @@ impl Command for History {
let Some(history) = engine_state.history_config() else { let Some(history) = engine_state.history_config() else {
return Ok(PipelineData::empty()); return Ok(PipelineData::empty());
}; };
// todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history` // todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history`
let Some(history_path) = history.file_path() else { if let Some(config_path) = nu_path::config_dir() {
return Err(ShellError::ConfigDirNotFound { span: Some(head) }); let clear = call.has_flag(engine_state, stack, "clear")?;
}; let long = call.has_flag(engine_state, stack, "long")?;
let ctrlc = engine_state.ctrlc.clone();
if call.has_flag(engine_state, stack, "clear")? { let mut history_path = config_path;
let _ = std::fs::remove_file(history_path); history_path.push("nushell");
// TODO: FIXME also clear the auxiliary files when using sqlite match history.file_format {
return Ok(PipelineData::empty()); HistoryFileFormat::Sqlite => {
} history_path.push("history.sqlite3");
}
HistoryFileFormat::PlainText => {
history_path.push("history.txt");
}
}
let long = call.has_flag(engine_state, stack, "long")?; if clear {
let signals = engine_state.signals().clone(); let _ = std::fs::remove_file(history_path);
let history_reader: Option<Box<dyn ReedlineHistory>> = match history.file_format { // TODO: FIXME also clear the auxiliary files when using sqlite
HistoryFileFormat::Sqlite => { Ok(PipelineData::empty())
SqliteBackedHistory::with_file(history_path.clone(), None, None) } else {
let history_reader: Option<Box<dyn ReedlineHistory>> = match history.file_format {
HistoryFileFormat::Sqlite => {
SqliteBackedHistory::with_file(history_path.clone(), None, None)
.map(|inner| {
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
boxed
})
.ok()
}
HistoryFileFormat::PlainText => FileBackedHistory::with_file(
history.max_size as usize,
history_path.clone(),
)
.map(|inner| { .map(|inner| {
let boxed: Box<dyn ReedlineHistory> = Box::new(inner); let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
boxed boxed
}) })
.ok() .ok(),
};
match history.file_format {
HistoryFileFormat::PlainText => Ok(history_reader
.and_then(|h| {
h.search(SearchQuery::everything(SearchDirection::Forward, None))
.ok()
})
.map(move |entries| {
entries.into_iter().enumerate().map(move |(idx, entry)| {
Value::record(
record! {
"command" => Value::string(entry.command_line, head),
"index" => Value::int(idx as i64, head),
},
head,
)
})
})
.ok_or(ShellError::FileNotFound {
file: history_path.display().to_string(),
span: head,
})?
.into_pipeline_data(ctrlc)),
HistoryFileFormat::Sqlite => Ok(history_reader
.and_then(|h| {
h.search(SearchQuery::everything(SearchDirection::Forward, None))
.ok()
})
.map(move |entries| {
entries.into_iter().enumerate().map(move |(idx, entry)| {
create_history_record(idx, entry, long, head)
})
})
.ok_or(ShellError::FileNotFound {
file: history_path.display().to_string(),
span: head,
})?
.into_pipeline_data(ctrlc)),
}
} }
HistoryFileFormat::Plaintext => { } else {
FileBackedHistory::with_file(history.max_size as usize, history_path.clone()) Err(ShellError::ConfigDirNotFound { span: Some(head) })
.map(|inner| {
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
boxed
})
.ok()
}
};
match history.file_format {
HistoryFileFormat::Plaintext => Ok(history_reader
.and_then(|h| {
h.search(SearchQuery::everything(SearchDirection::Forward, None))
.ok()
})
.map(move |entries| {
entries.into_iter().enumerate().map(move |(idx, entry)| {
Value::record(
record! {
fields::COMMAND_LINE => Value::string(entry.command_line, head),
// TODO: This name is inconsistent with create_history_record.
"index" => Value::int(idx as i64, head),
},
head,
)
})
})
.ok_or(IoError::new(
std::io::ErrorKind::NotFound,
head,
history_path,
))?
.into_pipeline_data(head, signals)),
HistoryFileFormat::Sqlite => Ok(history_reader
.and_then(|h| {
h.search(SearchQuery::everything(SearchDirection::Forward, None))
.ok()
})
.map(move |entries| {
entries.into_iter().enumerate().map(move |(idx, entry)| {
create_sqlite_history_record(idx, entry, long, head)
})
})
.ok_or(IoError::new(
std::io::ErrorKind::NotFound,
head,
history_path,
))?
.into_pipeline_data(head, signals)),
} }
} }
@ -139,36 +151,63 @@ impl Command for History {
} }
} }
fn create_sqlite_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) -> Value { fn create_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
let item_id_value = Value::int( let item_id_value = Value::int(
entry match entry.id {
.id Some(id) => {
.and_then(|id| id.to_string().parse::<i64>().ok()) let ids = id.to_string();
.unwrap_or_default(), match ids.parse::<i64>() {
Ok(i) => i,
_ => 0i64,
}
}
None => 0i64,
},
head, head,
); );
let start_timestamp_value = Value::date( let start_timestamp_value = Value::string(
entry.start_timestamp.unwrap_or_default().fixed_offset(), match entry.start_timestamp {
Some(time) => time.to_string(),
None => "".into(),
},
head, head,
); );
let command_value = Value::string(entry.command_line, head); let command_value = Value::string(entry.command_line, head);
let session_id_value = Value::int( let session_id_value = Value::int(
entry match entry.session_id {
.session_id Some(sid) => {
.and_then(|id| id.to_string().parse::<i64>().ok()) let sids = sid.to_string();
.unwrap_or_default(), match sids.parse::<i64>() {
Ok(i) => i,
_ => 0i64,
}
}
None => 0i64,
},
head,
);
let hostname_value = Value::string(
match entry.hostname {
Some(host) => host,
None => "".into(),
},
head,
);
let cwd_value = Value::string(
match entry.cwd {
Some(cwd) => cwd,
None => "".into(),
},
head, head,
); );
let hostname_value = Value::string(entry.hostname.unwrap_or_default(), head);
let cwd_value = Value::string(entry.cwd.unwrap_or_default(), head);
let duration_value = Value::duration( let duration_value = Value::duration(
entry match entry.duration {
.duration Some(d) => d.as_nanos().try_into().unwrap_or(0),
.and_then(|d| d.as_nanos().try_into().ok()) None => 0,
.unwrap_or(0), },
head, head,
); );
let exit_status_value = Value::int(entry.exit_status.unwrap_or(0), head); let exit_status_value = Value::int(entry.exit_status.unwrap_or(0), head);
@ -177,13 +216,13 @@ fn create_sqlite_history_record(idx: usize, entry: HistoryItem, long: bool, head
Value::record( Value::record(
record! { record! {
"item_id" => item_id_value, "item_id" => item_id_value,
fields::START_TIMESTAMP => start_timestamp_value, "start_timestamp" => start_timestamp_value,
fields::COMMAND_LINE => command_value, "command" => command_value,
fields::SESSION_ID => session_id_value, "session_id" => session_id_value,
fields::HOSTNAME => hostname_value, "hostname" => hostname_value,
fields::CWD => cwd_value, "cwd" => cwd_value,
fields::DURATION => duration_value, "duration" => duration_value,
fields::EXIT_STATUS => exit_status_value, "exit_status" => exit_status_value,
"idx" => index_value, "idx" => index_value,
}, },
head, head,
@ -191,11 +230,11 @@ fn create_sqlite_history_record(idx: usize, entry: HistoryItem, long: bool, head
} else { } else {
Value::record( Value::record(
record! { record! {
fields::START_TIMESTAMP => start_timestamp_value, "start_timestamp" => start_timestamp_value,
fields::COMMAND_LINE => command_value, "command" => command_value,
fields::CWD => cwd_value, "cwd" => cwd_value,
fields::DURATION => duration_value, "duration" => duration_value,
fields::EXIT_STATUS => exit_status_value, "exit_status" => exit_status_value,
}, },
head, head,
) )

View File

@ -1,441 +0,0 @@
use std::path::{Path, PathBuf};
use nu_engine::command_prelude::*;
use nu_protocol::{
shell_error::{self, io::IoError},
HistoryFileFormat,
};
use reedline::{
FileBackedHistory, History, HistoryItem, ReedlineError, SearchQuery, SqliteBackedHistory,
};
use super::fields;
#[derive(Clone)]
pub struct HistoryImport;
impl Command for HistoryImport {
fn name(&self) -> &str {
"history import"
}
fn description(&self) -> &str {
"Import command line history."
}
fn extra_description(&self) -> &str {
r#"Can import history from input, either successive command lines or more detailed records. If providing records, available fields are:
command_line, id, start_timestamp, hostname, cwd, duration, exit_status.
If no input is provided, will import all history items from existing history in the other format: if current history is stored in sqlite, it will store it in plain text and vice versa.
Note that history item IDs are ignored when importing from file."#
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("history import")
.category(Category::History)
.input_output_types(vec![
(Type::Nothing, Type::Nothing),
(Type::String, Type::Nothing),
(Type::List(Box::new(Type::String)), Type::Nothing),
(Type::table(), Type::Nothing),
])
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
example: "history import",
description:
"Append all items from history in the other format to the current history",
result: None,
},
Example {
example: "echo foo | history import",
description: "Append `foo` to the current history",
result: None,
},
Example {
example: "[[ command_line cwd ]; [ foo /home ]] | history import",
description: "Append `foo` ran from `/home` to the current history",
result: None,
},
]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let span = call.head;
let ok = Ok(Value::nothing(call.head).into_pipeline_data());
let Some(history) = engine_state.history_config() else {
return ok;
};
let Some(current_history_path) = history.file_path() else {
return Err(ShellError::ConfigDirNotFound { span: span.into() });
};
if let Some(bak_path) = backup(&current_history_path, span)? {
println!("Backed history to {}", bak_path.display());
}
match input {
PipelineData::Empty => {
let other_format = match history.file_format {
HistoryFileFormat::Sqlite => HistoryFileFormat::Plaintext,
HistoryFileFormat::Plaintext => HistoryFileFormat::Sqlite,
};
let src = new_backend(other_format, None)?;
let mut dst = new_backend(history.file_format, Some(current_history_path))?;
let items = src
.search(SearchQuery::everything(
reedline::SearchDirection::Forward,
None,
))
.map_err(error_from_reedline)?
.into_iter()
.map(Ok);
import(dst.as_mut(), items)
}
_ => {
let input = input.into_iter().map(item_from_value);
import(
new_backend(history.file_format, Some(current_history_path))?.as_mut(),
input,
)
}
}?;
ok
}
}
fn new_backend(
format: HistoryFileFormat,
path: Option<PathBuf>,
) -> Result<Box<dyn History>, ShellError> {
let path = match path {
Some(path) => path,
None => {
let Some(mut path) = nu_path::nu_config_dir() else {
return Err(ShellError::ConfigDirNotFound { span: None });
};
path.push(format.default_file_name());
path.into_std_path_buf()
}
};
fn map(
result: Result<impl History + 'static, ReedlineError>,
) -> Result<Box<dyn History>, ShellError> {
result
.map(|x| Box::new(x) as Box<dyn History>)
.map_err(error_from_reedline)
}
match format {
// Use a reasonably large value for maximum capacity.
HistoryFileFormat::Plaintext => map(FileBackedHistory::with_file(0xfffffff, path)),
HistoryFileFormat::Sqlite => map(SqliteBackedHistory::with_file(path, None, None)),
}
}
fn import(
dst: &mut dyn History,
src: impl Iterator<Item = Result<HistoryItem, ShellError>>,
) -> Result<(), ShellError> {
for item in src {
let mut item = item?;
item.id = None;
dst.save(item).map_err(error_from_reedline)?;
}
Ok(())
}
fn error_from_reedline(e: ReedlineError) -> ShellError {
// TODO: Should we add a new ShellError variant?
ShellError::GenericError {
error: "Reedline error".to_owned(),
msg: format!("{e}"),
span: None,
help: None,
inner: Vec::new(),
}
}
fn item_from_value(v: Value) -> Result<HistoryItem, ShellError> {
let span = v.span();
match v {
Value::Record { val, .. } => item_from_record(val.into_owned(), span),
Value::String { val, .. } => Ok(HistoryItem {
command_line: val,
id: None,
start_timestamp: None,
session_id: None,
hostname: None,
cwd: None,
duration: None,
exit_status: None,
more_info: None,
}),
_ => Err(ShellError::UnsupportedInput {
msg: "Only list and record inputs are supported".to_owned(),
input: v.get_type().to_string(),
msg_span: span,
input_span: span,
}),
}
}
fn item_from_record(mut rec: Record, span: Span) -> Result<HistoryItem, ShellError> {
let cmd = match rec.remove(fields::COMMAND_LINE) {
Some(v) => v.as_str()?.to_owned(),
None => {
return Err(ShellError::TypeMismatch {
err_message: format!("missing column: {}", fields::COMMAND_LINE),
span,
})
}
};
fn get<T>(
rec: &mut Record,
field: &'static str,
f: impl FnOnce(Value) -> Result<T, ShellError>,
) -> Result<Option<T>, ShellError> {
rec.remove(field).map(f).transpose()
}
let rec = &mut rec;
let item = HistoryItem {
command_line: cmd,
id: None,
start_timestamp: get(rec, fields::START_TIMESTAMP, |v| Ok(v.as_date()?.to_utc()))?,
hostname: get(rec, fields::HOSTNAME, |v| Ok(v.as_str()?.to_owned()))?,
cwd: get(rec, fields::CWD, |v| Ok(v.as_str()?.to_owned()))?,
exit_status: get(rec, fields::EXIT_STATUS, |v| v.as_int())?,
duration: get(rec, fields::DURATION, |v| duration_from_value(v, span))?,
more_info: None,
// TODO: Currently reedline doesn't let you create session IDs.
session_id: None,
};
if !rec.is_empty() {
let cols = rec.columns().map(|s| s.as_str()).collect::<Vec<_>>();
return Err(ShellError::TypeMismatch {
err_message: format!("unsupported column names: {}", cols.join(", ")),
span,
});
}
Ok(item)
}
fn duration_from_value(v: Value, span: Span) -> Result<std::time::Duration, ShellError> {
chrono::Duration::nanoseconds(v.as_duration()?)
.to_std()
.map_err(|_| ShellError::NeedsPositiveValue { span })
}
fn find_backup_path(path: &Path, span: Span) -> Result<PathBuf, ShellError> {
let Ok(mut bak_path) = path.to_path_buf().into_os_string().into_string() else {
// This isn't fundamentally problem, but trying to work with OsString is a nightmare.
return Err(ShellError::GenericError {
error: "History path not UTF-8".to_string(),
msg: "History path must be representable as UTF-8".to_string(),
span: Some(span),
help: None,
inner: vec![],
});
};
bak_path.push_str(".bak");
if !Path::new(&bak_path).exists() {
return Ok(bak_path.into());
}
let base_len = bak_path.len();
for i in 1..100 {
use std::fmt::Write;
bak_path.truncate(base_len);
write!(&mut bak_path, ".{i}").unwrap();
if !Path::new(&bak_path).exists() {
return Ok(PathBuf::from(bak_path));
}
}
Err(ShellError::GenericError {
error: "Too many backup files".to_string(),
msg: "Found too many existing backup files".to_string(),
span: Some(span),
help: None,
inner: vec![],
})
}
fn backup(path: &Path, span: Span) -> Result<Option<PathBuf>, ShellError> {
match path.metadata() {
Ok(md) if md.is_file() => (),
Ok(_) => {
return Err(IoError::new_with_additional_context(
shell_error::io::ErrorKind::NotAFile,
span,
PathBuf::from(path),
"history path exists but is not a file",
)
.into())
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(IoError::new_internal(
e.kind(),
"Could not get metadata",
nu_protocol::location!(),
)
.into())
}
}
let bak_path = find_backup_path(path, span)?;
std::fs::copy(path, &bak_path).map_err(|err| {
IoError::new_internal(
err.kind(),
"Could not copy backup",
nu_protocol::location!(),
)
})?;
Ok(Some(bak_path))
}
#[cfg(test)]
mod tests {
use chrono::DateTime;
use rstest::rstest;
use super::*;
#[test]
fn test_item_from_value_string() -> Result<(), ShellError> {
let item = item_from_value(Value::string("foo", Span::unknown()))?;
assert_eq!(
item,
HistoryItem {
command_line: "foo".to_string(),
id: None,
start_timestamp: None,
session_id: None,
hostname: None,
cwd: None,
duration: None,
exit_status: None,
more_info: None
}
);
Ok(())
}
#[test]
fn test_item_from_value_record() {
let span = Span::unknown();
let rec = new_record(&[
("command", Value::string("foo", span)),
(
"start_timestamp",
Value::date(
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap(),
span,
),
),
("hostname", Value::string("localhost", span)),
("cwd", Value::string("/home/test", span)),
("duration", Value::duration(100_000_000, span)),
("exit_status", Value::int(42, span)),
]);
let item = item_from_value(rec).unwrap();
assert_eq!(
item,
HistoryItem {
command_line: "foo".to_string(),
id: None,
start_timestamp: Some(
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")
.unwrap()
.to_utc()
),
hostname: Some("localhost".to_string()),
cwd: Some("/home/test".to_string()),
duration: Some(std::time::Duration::from_nanos(100_000_000)),
exit_status: Some(42),
session_id: None,
more_info: None
}
);
}
#[test]
fn test_item_from_value_record_extra_field() {
let span = Span::unknown();
let rec = new_record(&[
("command_line", Value::string("foo", span)),
("id_nonexistent", Value::int(1, span)),
]);
assert!(item_from_value(rec).is_err());
}
#[test]
fn test_item_from_value_record_bad_type() {
let span = Span::unknown();
let rec = new_record(&[
("command_line", Value::string("foo", span)),
("id", Value::string("one".to_string(), span)),
]);
assert!(item_from_value(rec).is_err());
}
fn new_record(rec: &[(&'static str, Value)]) -> Value {
let span = Span::unknown();
let rec = Record::from_raw_cols_vals(
rec.iter().map(|(col, _)| col.to_string()).collect(),
rec.iter().map(|(_, val)| val.clone()).collect(),
span,
span,
)
.unwrap();
Value::record(rec, span)
}
#[rstest]
#[case::no_backup(&["history.dat"], "history.dat.bak")]
#[case::backup_exists(&["history.dat", "history.dat.bak"], "history.dat.bak.1")]
#[case::multiple_backups_exists( &["history.dat", "history.dat.bak", "history.dat.bak.1"], "history.dat.bak.2")]
fn test_find_backup_path(#[case] existing: &[&str], #[case] want: &str) {
let dir = tempfile::tempdir().unwrap();
for name in existing {
std::fs::File::create_new(dir.path().join(name)).unwrap();
}
let got = find_backup_path(&dir.path().join("history.dat"), Span::test_data()).unwrap();
assert_eq!(got, dir.path().join(want))
}
#[test]
fn test_backup() {
let dir = tempfile::tempdir().unwrap();
let mut history = std::fs::File::create_new(dir.path().join("history.dat")).unwrap();
use std::io::Write;
write!(&mut history, "123").unwrap();
let want_bak_path = dir.path().join("history.dat.bak");
assert_eq!(
backup(&dir.path().join("history.dat"), Span::test_data()),
Ok(Some(want_bak_path.clone()))
);
let got_data = String::from_utf8(std::fs::read(want_bak_path).unwrap()).unwrap();
assert_eq!(got_data, "123");
}
#[test]
fn test_backup_no_file() {
let dir = tempfile::tempdir().unwrap();
let bak_path = backup(&dir.path().join("history.dat"), Span::test_data()).unwrap();
assert!(bak_path.is_none());
}
}

View File

@ -8,7 +8,7 @@ impl Command for HistorySession {
"history session" "history session"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Get the command history session." "Get the command history session."
} }

View File

@ -1,8 +1,5 @@
mod fields;
mod history_; mod history_;
mod history_import;
mod history_session; mod history_session;
pub use history_::History; pub use history_::History;
pub use history_import::HistoryImport;
pub use history_session::HistorySession; pub use history_session::HistorySession;

View File

@ -14,11 +14,11 @@ impl Command for Keybindings {
.input_output_types(vec![(Type::Nothing, Type::String)]) .input_output_types(vec![(Type::Nothing, Type::String)])
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Keybindings related commands." "Keybindings related commands."
} }
fn extra_description(&self) -> &str { fn extra_usage(&self) -> &str {
r#"You must use one of the following subcommands. Using this command as-is will only produce this help message. r#"You must use one of the following subcommands. Using this command as-is will only produce this help message.
For more information on input and keybindings, check: For more information on input and keybindings, check:
@ -36,6 +36,16 @@ For more information on input and keybindings, check:
call: &Call, call: &Call,
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) Ok(Value::string(
get_full_help(
&Keybindings.signature(),
&Keybindings.examples(),
engine_state,
stack,
self.is_parser_keyword(),
),
call.head,
)
.into_pipeline_data())
} }
} }

View File

@ -12,10 +12,10 @@ impl Command for KeybindingsDefault {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build(self.name()) Signature::build(self.name())
.category(Category::Platform) .category(Category::Platform)
.input_output_types(vec![(Type::Nothing, Type::table())]) .input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"List default keybindings." "List default keybindings."
} }

View File

@ -14,7 +14,7 @@ impl Command for KeybindingsList {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build(self.name()) Signature::build(self.name())
.input_output_types(vec![(Type::Nothing, Type::table())]) .input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
.switch("modifiers", "list of modifiers", Some('m')) .switch("modifiers", "list of modifiers", Some('m'))
.switch("keycodes", "list of keycodes", Some('k')) .switch("keycodes", "list of keycodes", Some('k'))
.switch("modes", "list of edit modes", Some('o')) .switch("modes", "list of edit modes", Some('o'))
@ -23,7 +23,7 @@ impl Command for KeybindingsList {
.category(Category::Platform) .category(Category::Platform)
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"List available options that can be used to create keybindings." "List available options that can be used to create keybindings."
} }
@ -49,26 +49,22 @@ impl Command for KeybindingsList {
fn run( fn run(
&self, &self,
engine_state: &EngineState, _engine_state: &EngineState,
stack: &mut Stack, _stack: &mut Stack,
call: &Call, call: &Call,
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let all_options = ["modifiers", "keycodes", "edits", "modes", "events"]; let records = if call.named_len() == 0 {
let all_options = ["modifiers", "keycodes", "edits", "modes", "events"];
let presence = all_options all_options
.iter() .iter()
.map(|option| call.has_flag(engine_state, stack, option)) .flat_map(|argument| get_records(argument, call.head))
.collect::<Result<Vec<_>, ShellError>>()?; .collect()
} else {
let no_option_specified = presence.iter().all(|present| !*present); call.named_iter()
.flat_map(|(argument, _, _)| get_records(argument.item.as_str(), call.head))
let records = all_options .collect()
.iter() };
.zip(presence)
.filter(|(_, present)| no_option_specified || *present)
.flat_map(|(option, _)| get_records(option, call.head))
.collect();
Ok(Value::list(records, call.head).into_pipeline_data()) Ok(Value::list(records, call.head).into_pipeline_data())
} }

View File

@ -2,7 +2,6 @@ use crossterm::{
event::Event, event::KeyCode, event::KeyEvent, execute, terminal, QueueableCommand, event::Event, event::KeyCode, event::KeyEvent, execute, terminal, QueueableCommand,
}; };
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::shell_error::io::IoError;
use std::io::{stdout, Write}; use std::io::{stdout, Write};
#[derive(Clone)] #[derive(Clone)]
@ -13,11 +12,11 @@ impl Command for KeybindingsListen {
"keybindings listen" "keybindings listen"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Get input from the user." "Get input from the user."
} }
fn extra_description(&self) -> &str { fn extra_usage(&self) -> &str {
"This is an internal debugging tool. For better output, try `input listen --types [key]`" "This is an internal debugging tool. For better output, try `input listen --types [key]`"
} }
@ -40,13 +39,7 @@ impl Command for KeybindingsListen {
match print_events(engine_state) { match print_events(engine_state) {
Ok(v) => Ok(v.into_pipeline_data()), Ok(v) => Ok(v.into_pipeline_data()),
Err(e) => { Err(e) => {
terminal::disable_raw_mode().map_err(|err| { terminal::disable_raw_mode()?;
IoError::new_internal(
err.kind(),
"Could not disable raw mode",
nu_protocol::location!(),
)
})?;
Err(ShellError::GenericError { Err(ShellError::GenericError {
error: "Error with input".into(), error: "Error with input".into(),
msg: "".into(), msg: "".into(),
@ -70,20 +63,8 @@ impl Command for KeybindingsListen {
pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> { pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
let config = engine_state.get_config(); let config = engine_state.get_config();
stdout().flush().map_err(|err| { stdout().flush()?;
IoError::new_internal( terminal::enable_raw_mode()?;
err.kind(),
"Could not flush stdout",
nu_protocol::location!(),
)
})?;
terminal::enable_raw_mode().map_err(|err| {
IoError::new_internal(
err.kind(),
"Could not enable raw mode",
nu_protocol::location!(),
)
})?;
if config.use_kitty_protocol { if config.use_kitty_protocol {
if let Ok(false) = crossterm::terminal::supports_keyboard_enhancement() { if let Ok(false) = crossterm::terminal::supports_keyboard_enhancement() {
@ -113,9 +94,7 @@ pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
let mut stdout = std::io::BufWriter::new(std::io::stderr()); let mut stdout = std::io::BufWriter::new(std::io::stderr());
loop { loop {
let event = crossterm::event::read().map_err(|err| { let event = crossterm::event::read()?;
IoError::new_internal(err.kind(), "Could not read event", nu_protocol::location!())
})?;
if event == Event::Key(KeyCode::Esc.into()) { if event == Event::Key(KeyCode::Esc.into()) {
break; break;
} }
@ -134,25 +113,9 @@ pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
_ => "".to_string(), _ => "".to_string(),
}; };
stdout.queue(crossterm::style::Print(o)).map_err(|err| { stdout.queue(crossterm::style::Print(o))?;
IoError::new_internal( stdout.queue(crossterm::style::Print("\r\n"))?;
err.kind(), stdout.flush()?;
"Could not print output record",
nu_protocol::location!(),
)
})?;
stdout
.queue(crossterm::style::Print("\r\n"))
.map_err(|err| {
IoError::new_internal(
err.kind(),
"Could not print linebreak",
nu_protocol::location!(),
)
})?;
stdout.flush().map_err(|err| {
IoError::new_internal(err.kind(), "Could not flush", nu_protocol::location!())
})?;
} }
if config.use_kitty_protocol { if config.use_kitty_protocol {
@ -162,13 +125,7 @@ pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
); );
} }
terminal::disable_raw_mode().map_err(|err| { terminal::disable_raw_mode()?;
IoError::new_internal(
err.kind(),
"Could not disable raw mode",
nu_protocol::location!(),
)
})?;
Ok(Value::nothing(Span::unknown())) Ok(Value::nothing(Span::unknown()))
} }

View File

@ -7,7 +7,7 @@ mod keybindings_list;
mod keybindings_listen; mod keybindings_listen;
pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor}; pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor};
pub use history::{History, HistoryImport, HistorySession}; pub use history::{History, HistorySession};
pub use keybindings::Keybindings; pub use keybindings::Keybindings;
pub use keybindings_default::KeybindingsDefault; pub use keybindings_default::KeybindingsDefault;
pub use keybindings_list::KeybindingsList; pub use keybindings_list::KeybindingsList;

View File

@ -1,87 +0,0 @@
use super::{completion_options::NuMatcher, SemanticSuggestion};
use crate::{
completions::{Completer, CompletionOptions},
SuggestionKind,
};
use nu_protocol::{
engine::{Stack, StateWorkingSet},
Span,
};
use reedline::Suggestion;
pub struct AttributeCompletion;
pub struct AttributableCompletion;
impl Completer for AttributeCompletion {
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(prefix, options);
let attr_commands =
working_set.find_commands_by_predicate(|s| s.starts_with(b"attr "), true);
for (decl_id, name, desc, ty) in attr_commands {
let name = name.strip_prefix(b"attr ").unwrap_or(&name);
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(name).into_owned(),
description: desc,
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: false,
},
kind: Some(SuggestionKind::Command(ty, Some(decl_id))),
});
}
matcher.results()
}
}
impl Completer for AttributableCompletion {
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(prefix, options);
for s in ["def", "extern", "export def", "export extern"] {
let decl_id = working_set
.find_decl(s.as_bytes())
.expect("internal error, builtin declaration not found");
let cmd = working_set.get_decl(decl_id);
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: cmd.name().into(),
description: Some(cmd.description().into()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: false,
},
kind: Some(SuggestionKind::Command(cmd.command_type(), None)),
});
}
matcher.results()
}
}

View File

@ -1,22 +1,45 @@
use crate::completions::CompletionOptions; use crate::completions::{CompletionOptions, SortBy};
use nu_protocol::{ use nu_protocol::{engine::StateWorkingSet, levenshtein_distance, Span};
engine::{Stack, StateWorkingSet},
DeclId, Span,
};
use reedline::Suggestion; use reedline::Suggestion;
// Completer trait represents the three stages of the completion
// fetch, filter and sort
pub trait Completer { pub trait Completer {
/// Fetch, filter, and sort completions
#[allow(clippy::too_many_arguments)]
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, prefix: Vec<u8>,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion>; ) -> Vec<SemanticSuggestion>;
fn get_sort_by(&self) -> SortBy {
SortBy::Ascending
}
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
let mut filtered_items = items;
// Sort items
match self.get_sort_by() {
SortBy::LevenshteinDistance => {
filtered_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value);
a_distance.cmp(&b_distance)
});
}
SortBy::Ascending => {
filtered_items.sort_by(|a, b| a.suggestion.value.cmp(&b.suggestion.value));
}
SortBy::None => {}
};
filtered_items
}
} }
#[derive(Debug, Default, PartialEq)] #[derive(Debug, Default, PartialEq)]
@ -28,15 +51,8 @@ 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, Option<DeclId>), Command(nu_protocol::engine::CommandType),
Value(nu_protocol::Type), Type(nu_protocol::Type),
CellPath,
Directory,
File,
Flag,
Module,
Operator,
Variable,
} }
impl From<Suggestion> for SemanticSuggestion { impl From<Suggestion> for SemanticSuggestion {

View File

@ -1,149 +0,0 @@
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{
ast::{Expr, Expression, FullCellPath, PathMember},
engine::{Stack, StateWorkingSet},
eval_const::eval_constant,
ShellError, Span, Value,
};
use reedline::Suggestion;
use super::completion_options::NuMatcher;
pub struct CellPathCompletion<'a> {
pub full_cell_path: &'a FullCellPath,
pub position: usize,
}
fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) {
let (prefix_str, start) = match member {
PathMember::String { val, span, .. } => (val, span.start),
PathMember::Int { val, span, .. } => (&val.to_string(), span.start),
};
let prefix_str = prefix_str.get(..pos + 1 - start).unwrap_or(prefix_str);
// strip wrapping quotes
let quotations = ['"', '\'', '`'];
let prefix_str = prefix_str.strip_prefix(quotations).unwrap_or(prefix_str);
(prefix_str.to_string(), Span::new(start, pos + 1))
}
impl Completer for CellPathCompletion<'_> {
fn fetch(
&mut self,
working_set: &StateWorkingSet,
stack: &Stack,
_prefix: impl AsRef<str>,
_span: Span,
offset: usize,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
let mut prefix_str = String::new();
// position at dots, e.g. `$env.config.<TAB>`
let mut span = Span::new(self.position + 1, self.position + 1);
let mut path_member_num_before_pos = 0;
for member in self.full_cell_path.tail.iter() {
if member.span().end <= self.position {
path_member_num_before_pos += 1;
} else if member.span().contains(self.position) {
(prefix_str, span) = prefix_from_path_member(member, self.position);
break;
}
}
let current_span = reedline::Span {
start: span.start - offset,
end: span.end - offset,
};
let mut matcher = NuMatcher::new(prefix_str, options);
let path_members = self
.full_cell_path
.tail
.get(0..path_member_num_before_pos)
.unwrap_or_default();
let value = eval_cell_path(
working_set,
stack,
&self.full_cell_path.head,
path_members,
span,
)
.unwrap_or_default();
for suggestion in get_suggestions_by_value(&value, current_span) {
matcher.add_semantic_suggestion(suggestion);
}
matcher.results()
}
}
/// Follow cell path to get the value
/// NOTE: This is a relatively lightweight implementation,
/// so it may fail to get the exact value when the expression is complicated.
/// One failing example would be `[$foo].0`
pub(crate) fn eval_cell_path(
working_set: &StateWorkingSet,
stack: &Stack,
head: &Expression,
path_members: &[PathMember],
span: Span,
) -> Result<Value, ShellError> {
// evaluate the head expression to get its value
let head_value = if let Expr::Var(var_id) = head.expr {
working_set
.get_variable(var_id)
.const_val
.to_owned()
.map_or_else(
|| eval_variable(working_set.permanent_state, stack, var_id, span),
Ok,
)
} else {
eval_constant(working_set, head)
}?;
head_value.follow_cell_path(path_members, false)
}
fn get_suggestions_by_value(
value: &Value,
current_span: reedline::Span,
) -> Vec<SemanticSuggestion> {
let to_suggestion = |s: String, v: Option<&Value>| {
// Check if the string needs quoting
let value = if s.is_empty()
|| s.chars()
.any(|c: char| !(c.is_ascii_alphabetic() || ['_', '-'].contains(&c)))
{
format!("{:?}", s)
} 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 {
Value::Record { val, .. } => val
.columns()
.map(|s| to_suggestion(s.to_string(), val.get(s)))
.collect(),
Value::List { vals, .. } => get_columns(vals.as_slice())
.into_iter()
.map(|s| {
let sub_val = vals
.first()
.and_then(|v| v.as_record().ok())
.and_then(|rv| rv.get(&s));
to_suggestion(s, sub_val)
})
.collect(),
_ => vec![],
}
}

View File

@ -1,86 +1,76 @@
use std::collections::HashMap;
use crate::{ use crate::{
completions::{Completer, CompletionOptions}, completions::{Completer, CompletionOptions, MatchAlgorithm, SortBy},
SuggestionKind, SuggestionKind,
}; };
use nu_parser::FlatShape;
use nu_protocol::{ use nu_protocol::{
engine::{CommandType, Stack, StateWorkingSet}, engine::{CachedFile, EngineState, StateWorkingSet},
Span, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::sync::Arc;
use super::{completion_options::NuMatcher, SemanticSuggestion}; use super::SemanticSuggestion;
pub struct CommandCompletion { pub struct CommandCompletion {
/// Whether to include internal commands engine_state: Arc<EngineState>,
pub internals: bool, flattened: Vec<(Span, FlatShape)>,
/// Whether to include external commands flat_shape: FlatShape,
pub externals: bool, force_completion_after_space: bool,
} }
impl CommandCompletion { impl CommandCompletion {
pub fn new(
engine_state: Arc<EngineState>,
_: &StateWorkingSet,
flattened: Vec<(Span, FlatShape)>,
flat_shape: FlatShape,
force_completion_after_space: bool,
) -> Self {
Self {
engine_state,
flattened,
flat_shape,
force_completion_after_space,
}
}
fn external_command_completion( fn external_command_completion(
&self, &self,
working_set: &StateWorkingSet, prefix: &str,
sugg_span: reedline::Span, match_algorithm: MatchAlgorithm,
matched_internal: impl Fn(&str) -> bool, ) -> Vec<String> {
matcher: &mut NuMatcher<String>, let mut executables = vec![];
) -> HashMap<String, SemanticSuggestion> {
let mut suggs = HashMap::new();
let paths = working_set.permanent_state.get_env_var_insensitive("path"); // os agnostic way to get the PATH env var
let paths = self.engine_state.get_path_env_var();
if let Some((_, paths)) = paths { if let Some(paths) = paths {
if let Ok(paths) = paths.as_list() { if let Ok(paths) = paths.as_list() {
for path in paths { for path in paths {
let path = path.coerce_str().unwrap_or_default(); let path = path.coerce_str().unwrap_or_default();
if let Ok(mut contents) = std::fs::read_dir(path.as_ref()) { if let Ok(mut contents) = std::fs::read_dir(path.as_ref()) {
while let Some(Ok(item)) = contents.next() { while let Some(Ok(item)) = contents.next() {
if working_set if self.engine_state.config.max_external_completion_results
.permanent_state > executables.len() as i64
.config && !executables.contains(
.completions &item
.external .path()
.max_results .file_name()
<= suggs.len() as i64 .map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(),
)
&& matches!(
item.path().file_name().map(|x| match_algorithm
.matches_str(&x.to_string_lossy(), prefix)),
Some(true)
)
&& is_executable::is_executable(item.path())
{ {
break; if let Ok(name) = item.file_name().into_string() {
} executables.push(name);
let Ok(name) = item.file_name().into_string() else { }
continue;
};
let value = if matched_internal(&name) {
format!("^{}", name)
} else {
name.clone()
};
if suggs.contains_key(&value) {
continue;
}
// TODO: check name matching before a relative heavy IO involved
// `is_executable` for performance consideration, should avoid
// duplicated `match_aux` call for matched items in the future
if matcher.matches(&name) && is_executable::is_executable(item.path()) {
// If there's an internal command with the same name, adds ^cmd to the
// matcher so that both the internal and external command are included
matcher.add(&name, value.clone());
suggs.insert(
value.clone(),
SemanticSuggestion {
suggestion: Suggestion {
value,
span: sugg_span,
append_whitespace: true,
..Default::default()
},
kind: Some(SuggestionKind::Command(
CommandType::External,
None,
)),
},
);
} }
} }
} }
@ -88,7 +78,82 @@ impl CommandCompletion {
} }
} }
suggs executables
}
fn complete_commands(
&self,
working_set: &StateWorkingSet,
span: Span,
offset: usize,
find_externals: bool,
match_algorithm: MatchAlgorithm,
) -> Vec<SemanticSuggestion> {
let partial = working_set.get_span_contents(span);
let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial);
let mut results = working_set
.find_commands_by_predicate(filter_predicate, true)
.into_iter()
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(),
description: x.1,
style: None,
extra: None,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
},
kind: Some(SuggestionKind::Command(x.2)),
})
.collect::<Vec<_>>();
let partial = working_set.get_span_contents(span);
let partial = String::from_utf8_lossy(partial).to_string();
if find_externals {
let results_external = self
.external_command_completion(&partial, match_algorithm)
.into_iter()
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x,
description: None,
style: None,
extra: None,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
},
// TODO: is there a way to create a test?
kind: None,
});
let results_strings: Vec<String> =
results.iter().map(|x| x.suggestion.value.clone()).collect();
for external in results_external {
if results_strings.contains(&external.suggestion.value) {
results.push(SemanticSuggestion {
suggestion: Suggestion {
value: format!("^{}", external.suggestion.value),
description: None,
style: None,
extra: None,
span: external.suggestion.span,
append_whitespace: true,
},
kind: external.kind,
})
} else {
results.push(external)
}
}
results
} else {
results
}
} }
} }
@ -96,63 +161,177 @@ impl Completer for CommandCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, _prefix: Vec<u8>,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let mut matcher = NuMatcher::new(prefix, options); let last = self
.flattened
.iter()
.rev()
.skip_while(|x| x.0.end > pos)
.take_while(|x| {
matches!(
x.1,
FlatShape::InternalCall(_)
| FlatShape::External
| FlatShape::ExternalArg
| FlatShape::Literal
| FlatShape::String
)
})
.last();
let sugg_span = reedline::Span::new(span.start - offset, span.end - offset); // The last item here would be the earliest shape that could possible by part of this subcommand
let subcommands = if let Some(last) = last {
let mut internal_suggs = HashMap::new(); self.complete_commands(
if self.internals {
let filtered_commands = working_set.find_commands_by_predicate(
|name| {
let name = String::from_utf8_lossy(name);
matcher.add(&name, name.to_string())
},
true,
);
for (decl_id, name, description, typ) in filtered_commands {
let name = String::from_utf8_lossy(&name);
internal_suggs.insert(
name.to_string(),
SemanticSuggestion {
suggestion: Suggestion {
value: name.to_string(),
description,
span: sugg_span,
append_whitespace: true,
..Suggestion::default()
},
kind: Some(SuggestionKind::Command(typ, Some(decl_id))),
},
);
}
}
let mut external_suggs = if self.externals {
self.external_command_completion(
working_set, working_set,
sugg_span, Span::new(last.0.start, pos),
|name| internal_suggs.contains_key(name), offset,
&mut matcher, false,
options.match_algorithm,
) )
} else { } else {
HashMap::new() vec![]
}; };
let mut res = Vec::new(); if !subcommands.is_empty() {
for cmd_name in matcher.results() { return subcommands;
if let Some(sugg) = internal_suggs }
.remove(&cmd_name)
.or_else(|| external_suggs.remove(&cmd_name)) let config = working_set.get_config();
{ let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External)
res.push(sugg); || matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_))
} || ((span.end - span.start) == 0)
|| is_passthrough_command(working_set.delta.get_file_contents())
{
// we're in a gap or at a command
if working_set.get_span_contents(span).is_empty() && !self.force_completion_after_space
{
return vec![];
}
self.complete_commands(
working_set,
span,
offset,
config.enable_external_completion,
options.match_algorithm,
)
} else {
vec![]
};
subcommands.into_iter().chain(commands).collect::<Vec<_>>()
}
fn get_sort_by(&self) -> SortBy {
SortBy::LevenshteinDistance
}
}
pub fn find_non_whitespace_index(contents: &[u8], start: usize) -> usize {
match contents.get(start..) {
Some(contents) => {
contents
.iter()
.take_while(|x| x.is_ascii_whitespace())
.count()
+ start
}
None => start,
}
}
pub fn is_passthrough_command(working_set_file_contents: &[CachedFile]) -> bool {
for cached_file in working_set_file_contents {
let contents = &cached_file.content;
let last_pipe_pos_rev = contents.iter().rev().position(|x| x == &b'|');
let last_pipe_pos = last_pipe_pos_rev.map(|x| contents.len() - x).unwrap_or(0);
let cur_pos = find_non_whitespace_index(contents, last_pipe_pos);
let result = match contents.get(cur_pos..) {
Some(contents) => contents.starts_with(b"sudo ") || contents.starts_with(b"doas "),
None => false,
};
if result {
return true;
}
}
false
}
#[cfg(test)]
mod command_completions_tests {
use super::*;
#[test]
fn test_find_non_whitespace_index() {
let commands = [
(" hello", 4),
("sudo ", 0),
(" sudo ", 2),
(" sudo ", 2),
(" hello ", 1),
(" hello ", 3),
(" hello | sudo ", 4),
(" sudo|sudo", 5),
("sudo | sudo ", 0),
(" hello sud", 1),
];
for (idx, ele) in commands.iter().enumerate() {
let index = find_non_whitespace_index(ele.0.as_bytes(), 0);
assert_eq!(index, ele.1, "Failed on index {}", idx);
}
}
#[test]
fn test_is_last_command_passthrough() {
let commands = [
(" hello", false),
(" sudo ", true),
("sudo ", true),
(" hello", false),
(" sudo", false),
(" sudo ", true),
(" sudo ", true),
(" sudo ", true),
(" hello ", false),
(" hello | sudo ", true),
(" sudo|sudo", false),
("sudo | sudo ", true),
(" hello sud", false),
(" sudo | sud ", false),
(" sudo|sudo ", true),
(" sudo | sudo ls | sudo ", true),
];
for (idx, ele) in commands.iter().enumerate() {
let input = ele.0.as_bytes();
let mut engine_state = EngineState::new();
engine_state.add_file("test.nu".into(), Arc::new([]));
let delta = {
let mut working_set = StateWorkingSet::new(&engine_state);
let _ = working_set.add_file("child.nu".into(), input);
working_set.render()
};
let result = engine_state.merge_delta(delta);
assert!(
result.is_ok(),
"Merge delta has failed: {}",
result.err().unwrap()
);
let is_passthrough_command = is_passthrough_command(engine_state.get_file_contents());
assert_eq!(
is_passthrough_command, ele.1,
"index for '{}': {}",
ele.0, idx
);
} }
res
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,156 +1,87 @@
use super::{completion_options::NuMatcher, MatchAlgorithm}; use crate::completions::{matches, CompletionOptions};
use crate::completions::CompletionOptions;
use nu_ansi_term::Style; use nu_ansi_term::Style;
use nu_engine::env_to_string; use nu_engine::env_to_string;
use nu_path::dots::expand_ndots; use nu_path::home_dir;
use nu_path::{expand_to_real_path, home_dir};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, Span,
}; };
use nu_utils::get_ls_colors; use nu_utils::get_ls_colors;
use nu_utils::IgnoreCaseExt; use std::{
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP}; ffi::OsStr,
path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP},
};
#[derive(Clone, Default)]
pub struct PathBuiltFromString {
cwd: PathBuf,
parts: Vec<String>,
isdir: bool,
}
/// Recursively goes through paths that match a given `partial`.
/// 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.
/// Parsing a path string into a pathbuf loses that bit of information.
///
/// `enable_exact_match`: Whether match algorithm is Prefix and all previous components
/// of the path matched a directory exactly.
fn complete_rec( fn complete_rec(
partial: &[&str], partial: &[String],
built_paths: &[PathBuiltFromString], cwd: &Path,
options: &CompletionOptions, options: &CompletionOptions,
want_directory: bool, dir: bool,
isdir: bool, isdir: bool,
enable_exact_match: bool, ) -> Vec<PathBuf> {
) -> Vec<PathBuiltFromString> { let mut completions = vec![];
if let Some((&base, rest)) = partial.split_first() {
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
let built_paths: Vec<_> = built_paths
.iter()
.map(|built| {
let mut built = built.clone();
built.parts.push(base.to_string());
built.isdir = true;
built
})
.collect();
return complete_rec(
rest,
&built_paths,
options,
want_directory,
isdir,
enable_exact_match,
);
}
}
let prefix = partial.first().unwrap_or(&"");
let mut matcher = NuMatcher::new(prefix, options);
for built in built_paths {
let mut path = built.cwd.clone();
for part in &built.parts {
path.push(part);
}
let Ok(result) = path.read_dir() else {
continue;
};
if let Ok(result) = cwd.read_dir() {
for entry in result.filter_map(|e| e.ok()) { for entry in result.filter_map(|e| e.ok()) {
let entry_name = entry.file_name().to_string_lossy().into_owned(); let entry_name = entry.file_name().to_string_lossy().into_owned();
let entry_isdir = entry.path().is_dir(); let path = entry.path();
let mut built = built.clone();
built.parts.push(entry_name.clone());
// Symlinks to directories shouldn't have a trailing slash (#13275)
built.isdir = entry_isdir && !entry.path().is_symlink();
if !want_directory || entry_isdir { if !dir || path.is_dir() {
matcher.add(entry_name.clone(), (entry_name, built)); match partial.first() {
} Some(base) if matches(base, &entry_name, options) => {
} let partial = &partial[1..];
} if !partial.is_empty() || isdir {
completions.extend(complete_rec(partial, &path, options, dir, isdir));
let mut completions = vec![]; if entry_name.eq(base) {
for (entry_name, built) in matcher.results() { break;
match partial.split_first() { }
Some((base, rest)) => {
// We use `isdir` to confirm that the current component has
// at least one next component or a slash.
// Serves as confirmation to ignore longer completions for
// components in between.
if !rest.is_empty() || isdir {
// 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 { } else {
entry_name.eq_ignore_case(base) completions.push(path)
}); }
completions.extend(complete_rec(
rest,
&[built],
options,
want_directory,
isdir,
exact_match,
));
if exact_match {
break;
} }
} else { None => completions.push(path),
completions.push(built); _ => {}
} }
} }
None => {
completions.push(built);
}
} }
} }
completions completions
} }
#[derive(Debug)]
enum OriginalCwd { enum OriginalCwd {
None, None,
Home, Home(PathBuf),
Prefix(String), Some(PathBuf),
// referencing a single local file
Local(PathBuf),
} }
impl OriginalCwd { impl OriginalCwd {
fn apply(&self, mut p: PathBuiltFromString, path_separator: char) -> String { fn apply(&self, p: &Path) -> String {
match self { let mut ret = match self {
Self::None => {} Self::None => p.to_string_lossy().into_owned(),
Self::Home => p.parts.insert(0, "~".to_string()), Self::Some(base) => pathdiff::diff_paths(p, base)
Self::Prefix(s) => p.parts.insert(0, s.clone()), .unwrap_or(p.to_path_buf())
.to_string_lossy()
.into_owned(),
Self::Home(home) => match p.strip_prefix(home) {
Ok(suffix) => format!("~{}{}", SEP, suffix.to_string_lossy()),
_ => p.to_string_lossy().into_owned(),
},
Self::Local(base) => Path::new(".")
.join(pathdiff::diff_paths(p, base).unwrap_or(p.to_path_buf()))
.to_string_lossy()
.into_owned(),
}; };
let mut ret = p.parts.join(&path_separator.to_string()); if p.is_dir() {
if p.isdir { ret.push(SEP);
ret.push(path_separator);
} }
ret ret
} }
} }
pub fn surround_remove(partial: &str) -> String { 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);
@ -164,158 +95,124 @@ pub fn surround_remove(partial: &str) -> String {
partial.to_string() partial.to_string()
} }
pub struct FileSuggestion {
pub span: nu_protocol::Span,
pub path: String,
pub style: Option<Style>,
pub is_dir: bool,
}
/// # Parameters
/// * `cwds` - A list of directories in which to search. The only reason this isn't a single string
/// is because dotnu_completions searches in multiple directories at once
pub fn complete_item( pub fn complete_item(
want_directory: bool, want_directory: bool,
span: nu_protocol::Span, span: nu_protocol::Span,
partial: &str, partial: &str,
cwds: &[impl AsRef<str>], cwd: &str,
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<FileSuggestion> { ) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
let cleaned_partial = surround_remove(partial); let partial = surround_remove(partial);
let isdir = cleaned_partial.ends_with(is_separator); let isdir = partial.ends_with(is_separator);
let expanded_partial = expand_ndots(Path::new(&cleaned_partial)); let cwd_pathbuf = Path::new(cwd).to_path_buf();
let should_collapse_dots = expanded_partial != Path::new(&cleaned_partial); let ls_colors = (engine_state.config.use_ls_colors_completions
let mut partial = expanded_partial.to_string_lossy().to_string(); && engine_state.config.use_ansi_coloring)
.then(|| {
#[cfg(unix)] let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
let path_separator = SEP; Some(v) => env_to_string("LS_COLORS", &v, engine_state, stack).ok(),
#[cfg(windows)] None => None,
let path_separator = cleaned_partial };
.chars() get_ls_colors(ls_colors_env_str)
.rfind(|c: &char| is_separator(*c)) });
.unwrap_or(SEP);
// Handle the trailing dot case
if cleaned_partial.ends_with(&format!("{path_separator}.")) {
partial.push_str(&format!("{path_separator}."));
}
let cwd_pathbufs: Vec<_> = cwds
.iter()
.map(|cwd| Path::new(cwd.as_ref()).to_path_buf())
.collect();
let ls_colors = (engine_state.config.completions.use_ls_colors
&& engine_state.config.use_ansi_coloring.get(engine_state))
.then(|| {
let ls_colors_env_str = stack
.get_env_var(engine_state, "LS_COLORS")
.and_then(|v| env_to_string("LS_COLORS", v, engine_state, stack).ok());
get_ls_colors(ls_colors_env_str)
});
let mut cwds = cwd_pathbufs.clone();
let mut prefix_len = 0;
let mut original_cwd = OriginalCwd::None; let mut original_cwd = OriginalCwd::None;
let mut components_vec: Vec<Component> = Path::new(&partial).components().collect();
let mut components = Path::new(&partial).components().peekable(); // Path components that end with a single "." get normalized away,
match components.peek().cloned() { // so if the partial path ends in a literal "." we must add it back in manually
if partial.ends_with('.') && partial.len() > 1 {
components_vec.push(Component::Normal(OsStr::new(".")));
};
let mut components = components_vec.into_iter().peekable();
let mut cwd = match components.peek().cloned() {
Some(c @ Component::Prefix(..)) => { Some(c @ Component::Prefix(..)) => {
// windows only by definition // windows only by definition
cwds = vec![[c, Component::RootDir].iter().collect()]; components.next();
prefix_len = c.as_os_str().len(); if let Some(Component::RootDir) = components.peek().cloned() {
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned()); components.next();
};
[c, Component::RootDir].iter().collect()
} }
Some(c @ Component::RootDir) => { Some(c @ Component::RootDir) => {
// This is kind of a hack. When joining an empty string with the rest, components.next();
// we add the slash automagically PathBuf::from(c.as_os_str())
cwds = vec![PathBuf::from(c.as_os_str())];
prefix_len = 1;
original_cwd = OriginalCwd::Prefix(String::new());
} }
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => { Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
cwds = home_dir() components.next();
.map(|dir| vec![dir.into()]) original_cwd = OriginalCwd::Home(home_dir().unwrap_or(cwd_pathbuf.clone()));
.unwrap_or(cwd_pathbufs); home_dir().unwrap_or(cwd_pathbuf)
prefix_len = 1; }
original_cwd = OriginalCwd::Home; Some(Component::CurDir) => {
components.next();
original_cwd = match components.peek().cloned() {
Some(Component::Normal(_)) | None => OriginalCwd::Local(cwd_pathbuf.clone()),
_ => OriginalCwd::Some(cwd_pathbuf.clone()),
};
cwd_pathbuf
}
_ => {
original_cwd = OriginalCwd::Some(cwd_pathbuf.clone());
cwd_pathbuf
} }
_ => {}
}; };
let after_prefix = &partial[prefix_len..]; let mut partial = vec![];
let partial: Vec<_> = after_prefix
.strip_prefix(is_separator)
.unwrap_or(after_prefix)
.split(is_separator)
.filter(|s| !s.is_empty())
.collect();
complete_rec( for component in components {
partial.as_slice(), match component {
&cwds Component::Prefix(..) => unreachable!(),
.into_iter() Component::RootDir => unreachable!(),
.map(|cwd| PathBuiltFromString { Component::CurDir => {}
cwd, Component::ParentDir => {
parts: Vec::new(), if partial.pop().is_none() {
isdir: false, cwd.pop();
}) }
.collect::<Vec<_>>(), }
options, Component::Normal(c) => partial.push(c.to_string_lossy().into_owned()),
want_directory,
isdir,
options.match_algorithm == MatchAlgorithm::Prefix,
)
.into_iter()
.map(|mut p| {
if should_collapse_dots {
p = collapse_ndots(p);
} }
let is_dir = p.isdir; }
let path = original_cwd.apply(p, path_separator);
let real_path = expand_to_real_path(&path); complete_rec(partial.as_slice(), &cwd, options, want_directory, isdir)
let metadata = std::fs::symlink_metadata(&real_path).ok(); .into_iter()
let style = ls_colors.as_ref().map(|lsc| { .map(|p| {
lsc.style_for_path_with_metadata(&real_path, metadata.as_ref()) let path = original_cwd.apply(&p);
let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata(
&path,
std::fs::symlink_metadata(&path).ok().as_ref(),
)
.map(lscolors::Style::to_nu_ansi_term_style) .map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default() .unwrap_or_default()
}); });
FileSuggestion { (span, escape_path(path, want_directory), style)
span, })
path: escape_path(path), .collect()
style,
is_dir,
}
})
.collect()
} }
// Fix files or folders with quotes or hashes // Fix files or folders with quotes or hashes
pub fn escape_path(path: String) -> String { pub fn escape_path(path: String, dir: bool) -> String {
// make glob pattern have the highest priority. // make glob pattern have the highest priority.
if nu_glob::is_glob(path.as_str()) || path.contains('`') { let glob_contaminated = path.contains(['[', '*', ']', '?']);
// expand home `~` for https://github.com/nushell/nushell/issues/13905 if glob_contaminated {
let pathbuf = nu_path::expand_tilde(path); return if path.contains('\'') {
let path = pathbuf.to_string_lossy(); // decide to use double quote, also need to escape `"` in path
if path.contains('\'') { // or else users can't do anything with completed path either.
// decide to use double quotes format!("\"{}\"", path.replace('"', r#"\""#))
// Path as Debug will do the escaping for `"`, `\`
format!("{:?}", path)
} else { } else {
format!("'{path}'") format!("'{path}'")
} };
}
let filename_contaminated = !dir && path.contains(['\'', '"', ' ', '#', '(', ')']);
let dirname_contaminated = dir && path.contains(['\'', '"', ' ', '#']);
let maybe_flag = path.starts_with('-');
let maybe_number = path.parse::<f64>().is_ok();
if filename_contaminated || dirname_contaminated || maybe_flag || maybe_number {
format!("`{path}`")
} else { } else {
let contaminated = path
path.contains(['\'', '"', ' ', '#', '(', ')', '{', '}', '[', ']', '|', ';']);
let maybe_flag = path.starts_with('-');
let maybe_variable = path.starts_with('$');
let maybe_number = path.parse::<f64>().is_ok();
if contaminated || maybe_flag || maybe_variable || maybe_number {
format!("`{path}`")
} else {
path
}
} }
} }
@ -326,12 +223,12 @@ pub struct AdjustView {
} }
pub fn adjust_if_intermediate( pub fn adjust_if_intermediate(
prefix: &str, prefix: &[u8],
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
mut span: nu_protocol::Span, mut span: nu_protocol::Span,
) -> AdjustView { ) -> AdjustView {
let span_contents = String::from_utf8_lossy(working_set.get_span_contents(span)).to_string(); let span_contents = String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
let mut prefix = prefix.to_string(); let mut prefix = String::from_utf8_lossy(prefix).to_string();
// A difference of 1 because of the cursor's unicode code point in between. // A difference of 1 because of the cursor's unicode code point in between.
// Using .chars().count() because unicode and Windows. // Using .chars().count() because unicode and Windows.
@ -351,38 +248,3 @@ pub fn adjust_if_intermediate(
readjusted, readjusted,
} }
} }
/// Collapse multiple ".." components into n-dots.
///
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
/// such as "..." and "....".
///
/// The resulting path will use platform-specific path separators, regardless of what path separators were used in the input.
fn collapse_ndots(path: PathBuiltFromString) -> PathBuiltFromString {
let mut result = PathBuiltFromString {
parts: Vec::with_capacity(path.parts.len()),
isdir: path.isdir,
cwd: path.cwd,
};
let mut dot_count = 0;
for part in path.parts {
if part == ".." {
dot_count += 1;
} else {
if dot_count > 0 {
result.parts.push(".".repeat(dot_count + 1));
dot_count = 0;
}
result.parts.push(part);
}
}
// Add any remaining dots
if dot_count > 0 {
result.parts.push(".".repeat(dot_count + 1));
}
result
}

View File

@ -1,16 +1,17 @@
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use nu_parser::trim_quotes_str; use nu_parser::trim_quotes_str;
use nu_protocol::{CompletionAlgorithm, CompletionSort}; use nu_protocol::CompletionAlgorithm;
use nu_utils::IgnoreCaseExt; use std::fmt::Display;
use nucleo_matcher::{
pattern::{Atom, AtomKind, CaseMatching, Normalization},
Config, Matcher, Utf32Str,
};
use std::{borrow::Cow, fmt::Display};
use super::SemanticSuggestion; #[derive(Copy, Clone)]
pub enum SortBy {
LevenshteinDistance,
Ascending,
None,
}
/// Describes how suggestions should be matched. /// Describes how suggestions should be matched.
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug)]
pub enum MatchAlgorithm { pub enum MatchAlgorithm {
/// Only show suggestions which begin with the given input /// Only show suggestions which begin with the given input
/// ///
@ -18,12 +19,6 @@ 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:
@ -31,203 +26,39 @@ pub enum MatchAlgorithm {
Fuzzy, Fuzzy,
} }
pub struct NuMatcher<'a, T> { impl MatchAlgorithm {
options: &'a CompletionOptions, /// Returns whether the `needle` search text matches the given `haystack`.
needle: String, pub fn matches_str(&self, haystack: &str, needle: &str) -> bool {
state: State<T>,
}
enum State<T> {
Prefix {
/// Holds (haystack, item)
items: Vec<(String, T)>,
},
Substring {
/// Holds (haystack, item)
items: Vec<(String, T)>,
},
Fuzzy {
matcher: Matcher,
atom: Atom,
/// Holds (haystack, item, score)
items: Vec<(String, T, u16)>,
},
}
/// Filters and sorts suggestions
impl<T> NuMatcher<'_, T> {
/// # Arguments
///
/// * `needle` - The text to search for
pub fn new(needle: impl AsRef<str>, options: &CompletionOptions) -> NuMatcher<T> {
let needle = trim_quotes_str(needle.as_ref());
match options.match_algorithm {
MatchAlgorithm::Prefix => {
let lowercase_needle = if options.case_sensitive {
needle.to_owned()
} else {
needle.to_folded_case()
};
NuMatcher {
options,
needle: lowercase_needle,
state: State::Prefix { items: Vec::new() },
}
}
MatchAlgorithm::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 => {
let atom = Atom::new(
needle,
if options.case_sensitive {
CaseMatching::Respect
} else {
CaseMatching::Ignore
},
Normalization::Smart,
AtomKind::Fuzzy,
false,
);
NuMatcher {
options,
needle: needle.to_owned(),
state: State::Fuzzy {
matcher: Matcher::new(Config::DEFAULT),
atom,
items: Vec::new(),
},
}
}
}
}
/// Returns whether or not the haystack matches the needle. If it does, `item` is added
/// to the list of matches (if given).
///
/// Helper to avoid code duplication between [NuMatcher::add] and [NuMatcher::matches].
fn matches_aux(&mut self, haystack: &str, item: Option<T>) -> bool {
let haystack = trim_quotes_str(haystack); let haystack = trim_quotes_str(haystack);
match &mut self.state { let needle = trim_quotes_str(needle);
State::Prefix { items } => { match *self {
let haystack_folded = if self.options.case_sensitive { MatchAlgorithm::Prefix => haystack.starts_with(needle),
Cow::Borrowed(haystack) MatchAlgorithm::Fuzzy => {
} else { let matcher = SkimMatcherV2::default();
Cow::Owned(haystack.to_folded_case()) matcher.fuzzy_match(haystack, needle).is_some()
};
let matches = 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 {
Cow::Owned(haystack.to_folded_case())
};
let matches = haystack_folded.contains(self.needle.as_str());
if matches {
if let Some(item) = item {
items.push((haystack.to_string(), item));
}
}
matches
}
State::Fuzzy {
matcher,
atom,
items,
} => {
let mut haystack_buf = Vec::new();
let haystack_utf32 = Utf32Str::new(trim_quotes_str(haystack), &mut haystack_buf);
let mut indices = Vec::new();
let Some(score) = atom.indices(haystack_utf32, matcher, &mut indices) else {
return false;
};
if let Some(item) = item {
items.push((haystack.to_string(), item, score));
}
true
} }
} }
} }
/// Add the given item if the given haystack matches the needle. /// Returns whether the `needle` search text matches the given `haystack`.
/// pub fn matches_u8(&self, haystack: &[u8], needle: &[u8]) -> bool {
/// Returns whether the item was added. match *self {
pub fn add(&mut self, haystack: impl AsRef<str>, item: T) -> bool { MatchAlgorithm::Prefix => haystack.starts_with(needle),
self.matches_aux(haystack.as_ref(), Some(item)) MatchAlgorithm::Fuzzy => {
} let haystack_str = String::from_utf8_lossy(haystack);
let needle_str = String::from_utf8_lossy(needle);
/// Returns whether the haystack matches the needle. let matcher = SkimMatcherV2::default();
pub fn matches(&mut self, haystack: &str) -> bool { matcher.fuzzy_match(&haystack_str, &needle_str).is_some()
self.matches_aux(haystack, None)
}
/// Get all the items that matched (sorted)
pub fn results(self) -> Vec<T> {
match self.state {
State::Prefix { mut items, .. } | State::Substring { mut items, .. } => {
items.sort_by(|(haystack1, _), (haystack2, _)| {
let cmp_sensitive = haystack1.cmp(haystack2);
if self.options.case_sensitive {
cmp_sensitive
} else {
haystack1
.to_folded_case()
.cmp(&haystack2.to_folded_case())
.then(cmp_sensitive)
}
});
items.into_iter().map(|(_, item)| item).collect::<Vec<_>>()
}
State::Fuzzy { mut items, .. } => {
match self.options.sort {
CompletionSort::Alphabetical => {
items.sort_by(|(haystack1, _, _), (haystack2, _, _)| {
haystack1.cmp(haystack2)
});
}
CompletionSort::Smart => {
items.sort_by(|(haystack1, _, score1), (haystack2, _, score2)| {
score2.cmp(score1).then(haystack1.cmp(haystack2))
});
}
}
items
.into_iter()
.map(|(_, item, _)| item)
.collect::<Vec<_>>()
} }
} }
} }
} }
impl NuMatcher<'_, SemanticSuggestion> {
pub fn add_semantic_suggestion(&mut self, sugg: SemanticSuggestion) -> bool {
let value = sugg.suggestion.value.to_string();
self.add(value, sugg)
}
}
impl From<CompletionAlgorithm> for MatchAlgorithm { 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,
} }
} }
@ -239,7 +70,6 @@ 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),
} }
@ -264,86 +94,51 @@ 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,
} }
impl Default for CompletionOptions { 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(),
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use rstest::rstest; use super::MatchAlgorithm;
use super::{CompletionOptions, MatchAlgorithm, NuMatcher}; #[test]
fn match_algorithm_prefix() {
let algorithm = MatchAlgorithm::Prefix;
#[rstest] assert!(algorithm.matches_str("example text", ""));
#[case(MatchAlgorithm::Prefix, "example text", "", true)] assert!(algorithm.matches_str("example text", "examp"));
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)] assert!(!algorithm.matches_str("example text", "text"));
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
#[case(MatchAlgorithm::Substring, "example text", "", true)] assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
#[case(MatchAlgorithm::Substring, "example text", "text", true)] assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
#[case(MatchAlgorithm::Substring, "example text", "mplxt", false)] assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "mplxt", true)]
#[case(MatchAlgorithm::Fuzzy, "example text", "mpp", false)]
fn match_algorithm_simple(
#[case] match_algorithm: MatchAlgorithm,
#[case] haystack: &str,
#[case] needle: &str,
#[case] should_match: bool,
) {
let options = CompletionOptions {
match_algorithm,
..Default::default()
};
let mut matcher = NuMatcher::new(needle, &options);
matcher.add(haystack, haystack);
if should_match {
assert_eq!(vec![haystack], matcher.results());
} else {
assert_ne!(vec![haystack], matcher.results());
}
} }
#[test] #[test]
fn match_algorithm_fuzzy_sort_score() { fn match_algorithm_fuzzy() {
let options = CompletionOptions { let algorithm = MatchAlgorithm::Fuzzy;
match_algorithm: MatchAlgorithm::Fuzzy,
..Default::default()
};
let mut matcher = NuMatcher::new("fob", &options);
for item in ["foo/bar", "fob", "foo bar"] {
matcher.add(item, item);
}
// Sort by score, then in alphabetical order
assert_eq!(vec!["fob", "foo bar", "foo/bar"], matcher.results());
}
#[test] assert!(algorithm.matches_str("example text", ""));
fn match_algorithm_fuzzy_sort_strip() { assert!(algorithm.matches_str("example text", "examp"));
let options = CompletionOptions { assert!(algorithm.matches_str("example text", "ext"));
match_algorithm: MatchAlgorithm::Fuzzy, assert!(algorithm.matches_str("example text", "mplxt"));
..Default::default() assert!(!algorithm.matches_str("example text", "mpp"));
};
let mut matcher = NuMatcher::new("'love spaces' ", &options); assert!(algorithm.matches_u8(&[1, 2, 3], &[]));
for item in [ assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 2]));
"'i love spaces'", assert!(algorithm.matches_u8(&[1, 2, 3], &[2, 3]));
"'i love spaces' so much", assert!(algorithm.matches_u8(&[1, 2, 3], &[1, 3]));
"'lovespaces' ", assert!(!algorithm.matches_u8(&[1, 2, 3], &[2, 2]));
] {
matcher.add(item, item);
}
// Make sure the spaces are respected
assert_eq!(vec!["'i love spaces' so much"], matcher.results());
} }
} }

View File

@ -1,166 +1,171 @@
use crate::completions::{ use crate::completions::{
completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm, completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm,
SemanticSuggestion, SemanticSuggestion, SortBy,
}; };
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::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
DeclId, PipelineData, Span, Type, Value, PipelineData, Span, Type, Value,
}; };
use std::collections::HashMap; use nu_utils::IgnoreCaseExt;
use std::{collections::HashMap, sync::Arc};
use super::completion_options::NuMatcher; pub struct CustomCompletion {
engine_state: Arc<EngineState>,
pub struct CustomCompletion<T: Completer> { stack: Stack,
decl_id: DeclId, decl_id: usize,
line: String, line: String,
line_pos: usize, sort_by: SortBy,
fallback: T,
} }
impl<T: Completer> CustomCompletion<T> { impl CustomCompletion {
pub fn new(decl_id: DeclId, line: String, line_pos: usize, fallback: T) -> Self { pub fn new(engine_state: Arc<EngineState>, stack: Stack, decl_id: usize, line: String) -> Self {
Self { Self {
engine_state,
stack: stack.reset_stdio().capture(),
decl_id, decl_id,
line, line,
line_pos, sort_by: SortBy::None,
fallback,
} }
} }
} }
impl<T: Completer> Completer for CustomCompletion<T> { impl Completer for CustomCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, _: &StateWorkingSet,
stack: &Stack, prefix: Vec<u8>,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
orig_options: &CompletionOptions, pos: usize,
completion_options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
// Call custom declaration // Line position
let mut stack_mut = stack.clone(); let line_pos = pos - offset;
let mut eval = |engine_state: &EngineState| {
eval_call::<WithoutDebug>(
engine_state,
&mut stack_mut,
&Call {
decl_id: self.decl_id,
head: span,
arguments: vec![
Argument::Positional(Expression::new_unknown(
Expr::String(self.line.clone()),
Span::unknown(),
Type::String,
)),
Argument::Positional(Expression::new_unknown(
Expr::Int(self.line_pos as i64),
Span::unknown(),
Type::Int,
)),
],
parser_info: HashMap::new(),
},
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(); // Call custom declaration
let mut should_sort = true; let result = eval_call::<WithoutDebug>(
&self.engine_state,
&mut self.stack,
&Call {
decl_id: self.decl_id,
head: span,
arguments: vec![
Argument::Positional(Expression {
span: Span::unknown(),
ty: Type::String,
expr: Expr::String(self.line.clone()),
custom_completion: None,
}),
Argument::Positional(Expression {
span: Span::unknown(),
ty: Type::Int,
expr: Expr::Int(line_pos as i64),
custom_completion: None,
}),
],
parser_info: HashMap::new(),
},
PipelineData::empty(),
);
let mut custom_completion_options = None;
// Parse result // Parse result
let suggestions = match result.and_then(|data| data.into_value(span)) { let suggestions = result
Ok(value) => match &value { .map(|pd| {
Value::Record { val, .. } => { let value = pd.into_value(span);
let completions = val match &value {
.get("completions") Value::Record { val, .. } => {
.and_then(|val| { let completions = val
val.as_list() .get("completions")
.ok() .and_then(|val| {
.map(|it| map_value_completions(it.iter(), span, offset)) val.as_list()
}) .ok()
.unwrap_or_default(); .map(|it| map_value_completions(it.iter(), span, offset))
let options = val.get("options"); })
.unwrap_or_default();
let options = val.get("options");
if let Some(Value::Record { val: options, .. }) = &options { if let Some(Value::Record { val: options, .. }) = &options {
if let Some(sort) = options.get("sort").and_then(|val| val.as_bool().ok()) { let should_sort = options
should_sort = sort; .get("sort")
} .and_then(|val| val.as_bool().ok())
.unwrap_or(false);
if let Some(case_sensitive) = options if should_sort {
.get("case_sensitive") self.sort_by = SortBy::Ascending;
.and_then(|val| val.as_bool().ok())
{
completion_options.case_sensitive = case_sensitive;
}
let positional =
options.get("positional").and_then(|val| val.as_bool().ok());
if positional.is_some() {
log::warn!("Use of the positional option is deprecated. Use the substring match algorithm instead.");
}
if let Some(algorithm) = options
.get("completion_algorithm")
.and_then(|option| option.coerce_string().ok())
.and_then(|option| option.try_into().ok())
{
completion_options.match_algorithm = algorithm;
if let Some(false) = positional {
if completion_options.match_algorithm == MatchAlgorithm::Prefix {
completion_options.match_algorithm = MatchAlgorithm::Substring
}
} }
custom_completion_options = Some(CompletionOptions {
case_sensitive: options
.get("case_sensitive")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
positional: options
.get("positional")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
match_algorithm: match options.get("completion_algorithm") {
Some(option) => option
.coerce_string()
.ok()
.and_then(|option| option.try_into().ok())
.unwrap_or(MatchAlgorithm::Prefix),
None => completion_options.match_algorithm,
},
});
} }
completions
} }
Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset),
completions _ => vec![],
} }
Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset), })
Value::Nothing { .. } => { .unwrap_or_default();
return self.fallback.fetch(
working_set,
stack,
prefix,
span,
offset,
orig_options,
);
}
_ => {
log::error!(
"Custom completer returned invalid value of type {}",
value.get_type().to_string()
);
return vec![];
}
},
Err(e) => {
log::error!("Error getting custom completions: {e}");
return vec![];
}
};
let mut matcher = NuMatcher::new(prefix, &completion_options); if let Some(custom_completion_options) = custom_completion_options {
filter(&prefix, suggestions, &custom_completion_options)
if should_sort {
for sugg in suggestions {
matcher.add_semantic_suggestion(sugg);
}
matcher.results()
} else { } else {
suggestions filter(&prefix, suggestions, completion_options)
.into_iter()
.filter(|sugg| matcher.matches(&sugg.suggestion.value))
.collect()
} }
} }
fn get_sort_by(&self) -> SortBy {
self.sort_by
}
}
fn filter(
prefix: &[u8],
items: Vec<SemanticSuggestion>,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
items
.into_iter()
.filter(|it| match options.match_algorithm {
MatchAlgorithm::Prefix => match (options.case_sensitive, options.positional) {
(true, true) => it.suggestion.value.as_bytes().starts_with(prefix),
(true, false) => it
.suggestion
.value
.contains(std::str::from_utf8(prefix).unwrap_or("")),
(false, positional) => {
let value = it.suggestion.value.to_folded_case();
let prefix = std::str::from_utf8(prefix).unwrap_or("").to_folded_case();
if positional {
value.starts_with(&prefix)
} else {
value.contains(&prefix)
}
}
},
MatchAlgorithm::Fuzzy => options
.match_algorithm
.matches_u8(it.suggestion.value.as_bytes(), prefix),
})
.collect()
} }

View File

@ -1,61 +1,109 @@
use crate::completions::{ use crate::completions::{
completion_common::{adjust_if_intermediate, complete_item, AdjustView}, completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, Completer, CompletionOptions, SortBy,
}; };
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, levenshtein_distance, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::path::Path; use std::{
path::{Path, MAIN_SEPARATOR as SEP},
sync::Arc,
};
use super::{completion_common::FileSuggestion, SemanticSuggestion, SuggestionKind}; use super::SemanticSuggestion;
pub struct DirectoryCompletion; #[derive(Clone)]
pub struct DirectoryCompletion {
engine_state: Arc<EngineState>,
stack: Stack,
}
impl DirectoryCompletion {
pub fn new(engine_state: Arc<EngineState>, stack: Stack) -> Self {
Self {
engine_state,
stack,
}
}
}
impl Completer for DirectoryCompletion { impl Completer for DirectoryCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, prefix: Vec<u8>,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let AdjustView { prefix, span, .. } = let AdjustView { prefix, span, .. } = adjust_if_intermediate(&prefix, working_set, span);
adjust_if_intermediate(prefix.as_ref(), working_set, span);
// Filter only the folders // Filter only the folders
#[allow(deprecated)] let output: Vec<_> = directory_completion(
let items: Vec<_> = directory_completion(
span, span,
&prefix, &prefix,
&working_set.permanent_state.current_work_dir(), &self.engine_state.current_work_dir(),
options, options,
working_set.permanent_state, self.engine_state.as_ref(),
stack, &self.stack,
) )
.into_iter() .into_iter()
.map(move |x| SemanticSuggestion { .map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.path, value: x.1,
style: x.style, description: None,
style: x.2,
extra: None,
span: reedline::Span { span: reedline::Span {
start: x.span.start - offset, start: x.0.start - offset,
end: x.span.end - offset, end: x.0.end - offset,
}, },
..Suggestion::default() append_whitespace: false,
}, },
kind: Some(SuggestionKind::Directory), // TODO????
kind: None,
}) })
.collect(); .collect();
output
}
// Sort results prioritizing the non hidden folders
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
// Sort items
let mut sorted_items = items;
match self.get_sort_by() {
SortBy::Ascending => {
sorted_items.sort_by(|a, b| {
// Ignore trailing slashes in folder names when sorting
a.suggestion
.value
.trim_end_matches(SEP)
.cmp(b.suggestion.value.trim_end_matches(SEP))
});
}
SortBy::LevenshteinDistance => {
sorted_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value);
a_distance.cmp(&b_distance)
});
}
_ => (),
}
// Separate the results between hidden and non hidden // Separate the results between hidden and non hidden
let mut hidden: Vec<SemanticSuggestion> = vec![]; let mut hidden: Vec<SemanticSuggestion> = vec![];
let mut non_hidden: Vec<SemanticSuggestion> = vec![]; let mut non_hidden: Vec<SemanticSuggestion> = vec![];
for item in items.into_iter() { for item in sorted_items.into_iter() {
let item_path = Path::new(&item.suggestion.value); let item_path = Path::new(&item.suggestion.value);
if let Some(value) = item_path.file_name() { if let Some(value) = item_path.file_name() {
@ -83,6 +131,6 @@ pub fn directory_completion(
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<FileSuggestion> { ) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
complete_item(true, span, partial, &[cwd], options, engine_state, stack) complete_item(true, span, partial, cwd, options, engine_state, stack)
} }

View File

@ -1,208 +1,148 @@
use crate::completions::{ use crate::completions::{file_path_completion, Completer, CompletionOptions, SortBy};
completion_common::{surround_remove, FileSuggestion},
completion_options::NuMatcher,
file_path_completion, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
};
use nu_path::expand_tilde;
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet, VirtualPath}, engine::{EngineState, Stack, StateWorkingSet},
Span, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::{ use std::{
collections::HashSet, path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR},
path::{is_separator, PathBuf, MAIN_SEPARATOR_STR}, sync::Arc,
}; };
use super::SemanticSuggestion;
#[derive(Clone)]
pub struct DotNuCompletion { pub struct DotNuCompletion {
/// e.g. use std/a<tab> engine_state: Arc<EngineState>,
pub std_virtual_path: bool, stack: Stack,
}
impl DotNuCompletion {
pub fn new(engine_state: Arc<EngineState>, stack: Stack) -> Self {
Self {
engine_state,
stack,
}
}
} }
impl Completer for DotNuCompletion { impl Completer for DotNuCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, _: &StateWorkingSet,
stack: &Stack, prefix: Vec<u8>,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let prefix_str = prefix.as_ref(); let prefix_str = String::from_utf8_lossy(&prefix).replace('`', "");
let start_with_backquote = prefix_str.starts_with('`'); let mut search_dirs: Vec<String> = vec![];
let end_with_backquote = prefix_str.ends_with('`');
let prefix_str = prefix_str.replace('`', "");
// e.g. `./`, `..\`, `/`
let not_lib_dirs = prefix_str
.chars()
.find(|c| *c != '.')
.is_some_and(is_separator);
let mut search_dirs: Vec<PathBuf> = vec![];
let (base, partial) = if let Some((parent, remain)) = prefix_str.rsplit_once(is_separator) { // If prefix_str is only a word we want to search in the current dir
// If prefix_str is only a word we want to search in the current dir. let (base, partial) = prefix_str
// "/xx" should be split to "/" and "xx". .rsplit_once(is_separator)
if parent.is_empty() { .unwrap_or((".", &prefix_str));
(MAIN_SEPARATOR_STR, remain)
} else {
(parent, remain)
}
} else {
(".", prefix_str.as_str())
};
let base_dir = base.replace(is_separator, MAIN_SEPARATOR_STR); let base_dir = base.replace(is_separator, MAIN_SEPARATOR_STR);
let mut partial = partial.to_string();
// On windows, this standardizes paths to use \
let mut is_current_folder = false;
// Fetch the lib dirs // Fetch the lib dirs
// NOTE: 2 ways to setup `NU_LIB_DIRS` let lib_dirs: Vec<String> =
// 1. `const NU_LIB_DIRS = [paths]`, equal to `nu -I paths` if let Some(lib_dirs) = self.engine_state.get_env_var("NU_LIB_DIRS") {
// 2. `$env.NU_LIB_DIRS = [paths]`
let const_lib_dirs = working_set
.find_variable(b"$NU_LIB_DIRS")
.and_then(|vid| working_set.get_variable(vid).const_val.as_ref());
let env_lib_dirs = working_set.get_env_var("NU_LIB_DIRS");
let lib_dirs: HashSet<PathBuf> = [const_lib_dirs, env_lib_dirs]
.into_iter()
.flatten()
.flat_map(|lib_dirs| {
lib_dirs lib_dirs
.as_list() .as_list()
.into_iter() .into_iter()
.flat_map(|it| it.iter().filter_map(|x| x.to_path().ok())) .flat_map(|it| {
.map(expand_tilde) it.iter().map(|x| {
}) x.to_path()
.collect(); .expect("internal error: failed to convert lib path")
})
})
.map(|it| {
it.into_os_string()
.into_string()
.expect("internal error: failed to convert OS path")
})
.collect()
} else {
vec![]
};
// Check if the base_dir is a folder // Check if the base_dir is a folder
let cwd = working_set.permanent_state.cwd(None); // rsplit_once removes the separator
if base_dir != "." { if base_dir != "." {
let expanded_base_dir = expand_tilde(&base_dir); // Add the base dir into the directories to be searched
let is_base_dir_relative = expanded_base_dir.is_relative(); search_dirs.push(base_dir.clone());
// Search in base_dir as well as lib_dirs.
// After expanded, base_dir can be a relative path or absolute path. // Reset the partial adding the basic dir back
// If relative, we join "current working dir" with it to get subdirectory and add to search_dirs. // in order to make the span replace work properly
// If absolute, we add it to search_dirs. let mut base_dir_partial = base_dir;
if let Ok(mut cwd) = cwd { base_dir_partial.push_str(&partial);
if is_base_dir_relative {
cwd.push(&base_dir); partial = base_dir_partial;
search_dirs.push(cwd.into_std_path_buf());
} else {
search_dirs.push(expanded_base_dir);
}
}
if !not_lib_dirs {
search_dirs.extend(lib_dirs.into_iter().map(|mut dir| {
dir.push(&base_dir);
dir
}));
}
} else { } else {
if let Ok(cwd) = cwd { // Fetch the current folder
search_dirs.push(cwd.into_std_path_buf()); let current_folder = self.engine_state.current_work_dir();
} is_current_folder = true;
if !not_lib_dirs {
search_dirs.extend(lib_dirs); // Add the current folder and the lib dirs into the
} // directories to be searched
search_dirs.push(current_folder);
search_dirs.extend(lib_dirs);
} }
// 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 mut completions = file_path_completion( let output: Vec<SemanticSuggestion> = search_dirs
span,
partial,
&search_dirs
.iter()
.filter_map(|d| d.to_str())
.collect::<Vec<_>>(),
options,
working_set.permanent_state,
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
.into_iter() .into_iter()
// Different base dir, so we list the .nu files or folders .flat_map(|search_dir| {
.filter(|it| { let completions = file_path_completion(
// for paths with spaces in them span,
let path = it.path.trim_end_matches('`'); &partial,
path.ends_with(".nu") || it.is_dir &search_dir,
options,
self.engine_state.as_ref(),
&self.stack,
);
completions
.into_iter()
.filter(move |it| {
// Different base dir, so we list the .nu files or folders
if !is_current_folder {
it.1.ends_with(".nu") || it.1.ends_with(SEP)
} else {
// Lib dirs, so we filter only the .nu files or directory modules
if it.1.ends_with(SEP) {
Path::new(&search_dir).join(&it.1).join("mod.nu").exists()
} else {
it.1.ends_with(".nu")
}
}
})
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
append_whitespace: true,
},
// TODO????
kind: None,
})
}) })
.map(|x| { .collect();
let append_whitespace = !x.is_dir && (!start_with_backquote || end_with_backquote);
// Re-calculate the span to replace output
let mut span_offset = 0; }
let mut value = x.path.to_string();
// Complete only the last path component fn get_sort_by(&self) -> SortBy {
if base_dir == MAIN_SEPARATOR_STR { SortBy::LevenshteinDistance
span_offset = base_dir.len()
} else if base_dir != "." {
span_offset = base_dir.len() + 1
}
// Retain only one '`'
if start_with_backquote {
value = value.trim_start_matches('`').to_string();
span_offset += 1;
}
// Add the backquote back
if end_with_backquote && !value.ends_with('`') {
value.push('`');
}
let end = x.span.end - offset;
let start = std::cmp::min(end, x.span.start - offset + span_offset);
SemanticSuggestion {
suggestion: Suggestion {
value,
style: x.style,
span: reedline::Span { start, end },
append_whitespace,
..Suggestion::default()
},
kind: Some(SuggestionKind::Module),
}
})
.collect::<Vec<_>>()
} }
} }

View File

@ -1,112 +0,0 @@
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
}
}

View File

@ -1,70 +1,114 @@
use crate::completions::{ use crate::completions::{
completion_common::{adjust_if_intermediate, complete_item, AdjustView}, completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, Completer, CompletionOptions, SortBy,
}; };
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, levenshtein_distance, Span,
}; };
use nu_utils::IgnoreCaseExt;
use reedline::Suggestion; use reedline::Suggestion;
use std::path::Path; use std::{
path::{Path, MAIN_SEPARATOR as SEP},
sync::Arc,
};
use super::{completion_common::FileSuggestion, SemanticSuggestion, SuggestionKind}; use super::SemanticSuggestion;
pub struct FileCompletion; #[derive(Clone)]
pub struct FileCompletion {
engine_state: Arc<EngineState>,
stack: Stack,
}
impl FileCompletion {
pub fn new(engine_state: Arc<EngineState>, stack: Stack) -> Self {
Self {
engine_state,
stack,
}
}
}
impl Completer for FileCompletion { impl Completer for FileCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack, prefix: Vec<u8>,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let AdjustView { let AdjustView {
prefix, prefix,
span, span,
readjusted, readjusted,
} = adjust_if_intermediate(prefix.as_ref(), working_set, span); } = adjust_if_intermediate(&prefix, working_set, span);
#[allow(deprecated)] let output: Vec<_> = complete_item(
let items: Vec<_> = complete_item(
readjusted, readjusted,
span, span,
&prefix, &prefix,
&[&working_set.permanent_state.current_work_dir()], &self.engine_state.current_work_dir(),
options, options,
working_set.permanent_state, self.engine_state.as_ref(),
stack, &self.stack,
) )
.into_iter() .into_iter()
.map(move |x| SemanticSuggestion { .map(move |x| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: x.path, value: x.1,
style: x.style, description: None,
style: x.2,
extra: None,
span: reedline::Span { span: reedline::Span {
start: x.span.start - offset, start: x.0.start - offset,
end: x.span.end - offset, end: x.0.end - offset,
}, },
..Suggestion::default() append_whitespace: false,
}, },
kind: Some(if x.is_dir { // TODO????
SuggestionKind::Directory kind: None,
} else {
SuggestionKind::File
}),
}) })
.collect(); .collect();
// Sort results prioritizing the non hidden folders output
}
// Sort results prioritizing the non hidden folders
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
// Sort items
let mut sorted_items = items;
match self.get_sort_by() {
SortBy::Ascending => {
sorted_items.sort_by(|a, b| {
// Ignore trailing slashes in folder names when sorting
a.suggestion
.value
.trim_end_matches(SEP)
.cmp(b.suggestion.value.trim_end_matches(SEP))
});
}
SortBy::LevenshteinDistance => {
sorted_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value);
a_distance.cmp(&b_distance)
});
}
_ => (),
}
// Separate the results between hidden and non hidden // Separate the results between hidden and non hidden
let mut hidden: Vec<SemanticSuggestion> = vec![]; let mut hidden: Vec<SemanticSuggestion> = vec![];
let mut non_hidden: Vec<SemanticSuggestion> = vec![]; let mut non_hidden: Vec<SemanticSuggestion> = vec![];
for item in items.into_iter() { for item in sorted_items.into_iter() {
let item_path = Path::new(&item.suggestion.value); let item_path = Path::new(&item.suggestion.value);
if let Some(value) = item_path.file_name() { if let Some(value) = item_path.file_name() {
@ -88,10 +132,21 @@ impl Completer for FileCompletion {
pub fn file_path_completion( pub fn file_path_completion(
span: nu_protocol::Span, span: nu_protocol::Span,
partial: &str, partial: &str,
cwds: &[impl AsRef<str>], cwd: &str,
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, engine_state: &EngineState,
stack: &Stack, stack: &Stack,
) -> Vec<FileSuggestion> { ) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
complete_item(false, span, partial, cwds, options, engine_state, stack) complete_item(false, span, partial, cwd, options, engine_state, stack)
}
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
// Check for case sensitive
if !options.case_sensitive {
return options
.match_algorithm
.matches_str(&from.to_folded_case(), &partial.to_folded_case());
}
options.match_algorithm.matches_str(from, partial)
} }

View File

@ -1,58 +1,97 @@
use crate::completions::{ use crate::completions::{Completer, CompletionOptions};
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
};
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet}, ast::{Expr, Expression},
DeclId, Span, engine::StateWorkingSet,
Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use super::SemanticSuggestion;
#[derive(Clone)] #[derive(Clone)]
pub struct FlagCompletion { pub struct FlagCompletion {
pub decl_id: DeclId, expression: Expression,
}
impl FlagCompletion {
pub fn new(expression: Expression) -> Self {
Self { expression }
}
} }
impl Completer for FlagCompletion { impl Completer for FlagCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, prefix: Vec<u8>,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let mut matcher = NuMatcher::new(prefix, options); // Check if it's a flag
let mut add_suggestion = |value: String, description: String| { if let Expr::Call(call) = &self.expression.expr {
matcher.add_semantic_suggestion(SemanticSuggestion { let decl = working_set.get_decl(call.decl_id);
suggestion: Suggestion { let sig = decl.signature();
value,
description: Some(description),
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
..Suggestion::default()
},
kind: Some(SuggestionKind::Flag),
});
};
let decl = working_set.get_decl(self.decl_id); let mut output = vec![];
let sig = decl.signature();
for named in &sig.named { for named in &sig.named {
if let Some(short) = named.short { let flag_desc = &named.desc;
let mut name = String::from("-"); if let Some(short) = named.short {
name.push(short); let mut named = vec![0; short.len_utf8()];
add_suggestion(name, named.desc.clone()); short.encode_utf8(&mut named);
named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
},
// TODO????
kind: None,
});
}
}
if named.long.is_empty() {
continue;
}
let mut named = named.long.as_bytes().to_vec();
named.insert(0, b'-');
named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
},
// TODO????
kind: None,
});
}
} }
if named.long.is_empty() { return output;
continue;
}
add_suggestion(format!("--{}", named.long), named.desc.clone());
} }
matcher.results()
vec![]
} }
} }

View File

@ -1,6 +1,4 @@
mod attribute_completions;
mod base; mod base;
mod cell_path_completions;
mod command_completions; mod command_completions;
mod completer; mod completer;
mod completion_common; mod completion_common;
@ -8,23 +6,17 @@ 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 variable_completions; mod variable_completions;
pub use attribute_completions::{AttributableCompletion, AttributeCompletion};
pub use base::{Completer, SemanticSuggestion, SuggestionKind}; pub use base::{Completer, SemanticSuggestion, SuggestionKind};
pub use cell_path_completions::CellPathCompletion;
pub use command_completions::CommandCompletion; pub use command_completions::CommandCompletion;
pub use completer::NuCompleter; pub use completer::NuCompleter;
pub use completion_options::{CompletionOptions, MatchAlgorithm}; pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy};
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, matches, FileCompletion};
pub use file_completions::{file_path_completion, FileCompletion};
pub use flag_completions::FlagCompletion; pub use flag_completions::FlagCompletion;
pub use operator_completions::OperatorCompletion;
pub use variable_completions::VariableCompletion; pub use variable_completions::VariableCompletion;

View File

@ -1,277 +0,0 @@
use crate::completions::{
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
};
use nu_protocol::{
ast::{self, Comparison, Expr, Expression},
engine::{Stack, StateWorkingSet},
Span, Type, Value, ENV_VARIABLE_ID,
};
use reedline::Suggestion;
use strum::{EnumMessage, IntoEnumIterator};
use super::cell_path_completions::eval_cell_path;
#[derive(Clone)]
pub struct OperatorCompletion<'a> {
pub left_hand_side: &'a Expression,
}
struct OperatorItem {
pub symbols: String,
pub description: String,
}
fn operator_to_item<T: EnumMessage + AsRef<str>>(op: T) -> OperatorItem {
OperatorItem {
symbols: op.as_ref().into(),
description: op.get_message().unwrap_or_default().into(),
}
}
fn common_comparison_ops() -> Vec<OperatorItem> {
vec![
operator_to_item(Comparison::In),
operator_to_item(Comparison::NotIn),
operator_to_item(Comparison::Equal),
operator_to_item(Comparison::NotEqual),
]
}
fn all_ops_for_immutable() -> Vec<OperatorItem> {
ast::Comparison::iter()
.map(operator_to_item)
.chain(ast::Math::iter().map(operator_to_item))
.chain(ast::Boolean::iter().map(operator_to_item))
.chain(ast::Bits::iter().map(operator_to_item))
.collect()
}
fn collection_comparison_ops() -> Vec<OperatorItem> {
let mut ops = common_comparison_ops();
ops.push(operator_to_item(Comparison::Has));
ops.push(operator_to_item(Comparison::NotHas));
ops
}
fn number_comparison_ops() -> Vec<OperatorItem> {
Comparison::iter()
.filter(|op| {
!matches!(
op,
Comparison::RegexMatch
| Comparison::NotRegexMatch
| Comparison::StartsWith
| Comparison::EndsWith
| Comparison::Has
| Comparison::NotHas
)
})
.map(operator_to_item)
.collect()
}
fn math_ops() -> Vec<OperatorItem> {
ast::Math::iter()
.filter(|op| !matches!(op, ast::Math::Concatenate | ast::Math::Pow))
.map(operator_to_item)
.collect()
}
fn bit_ops() -> Vec<OperatorItem> {
ast::Bits::iter().map(operator_to_item).collect()
}
fn all_assignment_ops() -> Vec<OperatorItem> {
ast::Assignment::iter().map(operator_to_item).collect()
}
fn numeric_assignment_ops() -> Vec<OperatorItem> {
ast::Assignment::iter()
.filter(|op| !matches!(op, ast::Assignment::ConcatenateAssign))
.map(operator_to_item)
.collect()
}
fn concat_assignment_ops() -> Vec<OperatorItem> {
vec![
operator_to_item(ast::Assignment::Assign),
operator_to_item(ast::Assignment::ConcatenateAssign),
]
}
fn valid_int_ops() -> Vec<OperatorItem> {
let mut ops = valid_float_ops();
ops.extend(bit_ops());
ops
}
fn valid_float_ops() -> Vec<OperatorItem> {
let mut ops = valid_value_with_unit_ops();
ops.push(operator_to_item(ast::Math::Pow));
ops
}
fn valid_string_ops() -> Vec<OperatorItem> {
let mut ops: Vec<OperatorItem> = Comparison::iter().map(operator_to_item).collect();
ops.push(operator_to_item(ast::Math::Concatenate));
ops.push(OperatorItem {
symbols: "like".into(),
description: Comparison::RegexMatch
.get_message()
.unwrap_or_default()
.into(),
});
ops.push(OperatorItem {
symbols: "not-like".into(),
description: Comparison::NotRegexMatch
.get_message()
.unwrap_or_default()
.into(),
});
ops
}
fn valid_list_ops() -> Vec<OperatorItem> {
let mut ops = collection_comparison_ops();
ops.push(operator_to_item(ast::Math::Concatenate));
ops
}
fn valid_binary_ops() -> Vec<OperatorItem> {
let mut ops = number_comparison_ops();
ops.extend(bit_ops());
ops.push(operator_to_item(ast::Math::Concatenate));
ops
}
fn valid_bool_ops() -> Vec<OperatorItem> {
let mut ops: Vec<OperatorItem> = ast::Boolean::iter().map(operator_to_item).collect();
ops.extend(common_comparison_ops());
ops
}
fn valid_value_with_unit_ops() -> Vec<OperatorItem> {
let mut ops = number_comparison_ops();
ops.extend(math_ops());
ops
}
fn ops_by_value(value: &Value, mutable: bool) -> Vec<OperatorItem> {
let mut ops = match value {
Value::Int { .. } => valid_int_ops(),
Value::Float { .. } => valid_float_ops(),
Value::String { .. } => valid_string_ops(),
Value::Binary { .. } => valid_binary_ops(),
Value::Bool { .. } => valid_bool_ops(),
Value::Date { .. } => number_comparison_ops(),
Value::Filesize { .. } | Value::Duration { .. } => valid_value_with_unit_ops(),
Value::Range { .. } | Value::Record { .. } => collection_comparison_ops(),
Value::List { .. } => valid_list_ops(),
_ => all_ops_for_immutable(),
};
if mutable {
ops.extend(match value {
Value::Int { .. }
| Value::Float { .. }
| Value::Filesize { .. }
| Value::Duration { .. } => numeric_assignment_ops(),
Value::String { .. } | Value::Binary { .. } | Value::List { .. } => {
concat_assignment_ops()
}
Value::Bool { .. }
| Value::Date { .. }
| Value::Range { .. }
| Value::Record { .. } => vec![operator_to_item(ast::Assignment::Assign)],
_ => all_assignment_ops(),
})
}
ops
}
fn is_expression_mutable(expr: &Expr, working_set: &StateWorkingSet) -> bool {
let Expr::FullCellPath(path) = expr else {
return false;
};
let Expr::Var(id) = path.head.expr else {
return false;
};
if id == ENV_VARIABLE_ID {
return true;
}
let var = working_set.get_variable(id);
var.mutable
}
impl Completer for OperatorCompletion<'_> {
fn fetch(
&mut self,
working_set: &StateWorkingSet,
stack: &Stack,
prefix: impl AsRef<str>,
span: Span,
offset: usize,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
let mut needs_assignment_ops = true;
// Complete according expression type
// TODO: type inference on self.left_hand_side to get more accurate completions
let mut possible_operations: Vec<OperatorItem> = match &self.left_hand_side.ty {
Type::Int | Type::Number => valid_int_ops(),
Type::Float => valid_float_ops(),
Type::String => valid_string_ops(),
Type::Binary => valid_binary_ops(),
Type::Bool => valid_bool_ops(),
Type::Date => number_comparison_ops(),
Type::Filesize | Type::Duration => valid_value_with_unit_ops(),
Type::Record(_) | Type::Range => collection_comparison_ops(),
Type::List(_) | Type::Table(_) => valid_list_ops(),
// Unknown type, resort to evaluated values
Type::Any => match &self.left_hand_side.expr {
Expr::FullCellPath(path) => {
// for `$ <tab>`
if matches!(path.head.expr, Expr::Garbage) {
return vec![];
}
let value =
eval_cell_path(working_set, stack, &path.head, &path.tail, path.head.span)
.unwrap_or_default();
let mutable = is_expression_mutable(&self.left_hand_side.expr, working_set);
// to avoid duplication
needs_assignment_ops = false;
ops_by_value(&value, mutable)
}
_ => all_ops_for_immutable(),
},
_ => common_comparison_ops(),
};
// If the left hand side is a variable, add assignment operators if mutable
if needs_assignment_ops && is_expression_mutable(&self.left_hand_side.expr, working_set) {
possible_operations.extend(match &self.left_hand_side.ty {
Type::Int | Type::Float | Type::Number => numeric_assignment_ops(),
Type::Filesize | Type::Duration => numeric_assignment_ops(),
Type::String | Type::Binary | Type::List(_) => concat_assignment_ops(),
Type::Any => all_assignment_ops(),
_ => vec![operator_to_item(ast::Assignment::Assign)],
});
}
let mut matcher = NuMatcher::new(prefix, options);
for OperatorItem {
symbols,
description,
} in possible_operations
{
matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion {
value: symbols.to_owned(),
description: Some(description.to_owned()),
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
..Suggestion::default()
},
kind: Some(SuggestionKind::Operator),
});
}
matcher.results()
}
}

View File

@ -1,55 +1,183 @@
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind}; use crate::completions::{
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind,
};
use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, VarId, Span, Value,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::{str, sync::Arc};
use super::completion_options::NuMatcher; #[derive(Clone)]
pub struct VariableCompletion {
engine_state: Arc<EngineState>, // TODO: Is engine state necessary? It's already a part of working set in fetch()
stack: Stack,
var_context: (Vec<u8>, Vec<Vec<u8>>), // tuple with $var and the sublevels (.b.c.d)
}
pub struct VariableCompletion; impl VariableCompletion {
pub fn new(
engine_state: Arc<EngineState>,
stack: Stack,
var_context: (Vec<u8>, Vec<Vec<u8>>),
) -> Self {
Self {
engine_state,
stack,
var_context,
}
}
}
impl Completer for VariableCompletion { impl Completer for VariableCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, prefix: Vec<u8>,
prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
_: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let mut matcher = NuMatcher::new(prefix, options); let mut output = vec![];
let builtins = ["$nu", "$in", "$env"];
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
let var_id = working_set.find_variable(&self.var_context.0);
let current_span = reedline::Span { let current_span = reedline::Span {
start: span.start - offset, start: span.start - offset,
end: span.end - offset, end: span.end - offset,
}; };
let sublevels_count = self.var_context.1.len();
// Variable completion (e.g: $en<tab> to complete $env) // Completions for the given variable
let builtins = ["$nu", "$in", "$env"]; if !var_str.is_empty() {
for builtin in builtins { // Completion for $env.<tab>
matcher.add_semantic_suggestion(SemanticSuggestion { if var_str == "$env" {
suggestion: Suggestion { let env_vars = self.stack.get_env_vars(&self.engine_state);
value: builtin.to_string(),
span: current_span, // Return nested values
description: Some("reserved".into()), if sublevels_count > 0 {
..Suggestion::default() // Extract the target var ($env.<target-var>)
}, let target_var = self.var_context.1[0].clone();
kind: Some(SuggestionKind::Variable), let target_var_str =
}); str::from_utf8(&target_var).unwrap_or_default().to_string();
// Everything after the target var is the nested level ($env.<target-var>.<nested_levels>...)
let nested_levels: Vec<Vec<u8>> =
self.var_context.1.clone().into_iter().skip(1).collect();
if let Some(val) = env_vars.get(&target_var_str) {
for suggestion in
nested_suggestions(val.clone(), nested_levels, current_span)
{
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
&prefix,
) {
output.push(suggestion);
}
}
return output;
}
} else {
// No nesting provided, return all env vars
for env_var in env_vars {
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
env_var.0.as_bytes(),
&prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: env_var.0,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
});
}
}
return output;
}
}
// Completions for $nu.<tab>
if var_str == "$nu" {
// Eval nu var
if let Ok(nuval) = eval_variable(
&self.engine_state,
&self.stack,
nu_protocol::NU_VARIABLE_ID,
nu_protocol::Span::new(current_span.start, current_span.end),
) {
for suggestion in
nested_suggestions(nuval, self.var_context.1.clone(), current_span)
{
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
&prefix,
) {
output.push(suggestion);
}
}
return output;
}
}
// Completion other variable types
if let Some(var_id) = var_id {
// Extract the variable value from the stack
let var = self.stack.get_var(var_id, Span::new(span.start, span.end));
// If the value exists and it's of type Record
if let Ok(value) = var {
for suggestion in
nested_suggestions(value, self.var_context.1.clone(), current_span)
{
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.suggestion.value.as_bytes(),
&prefix,
) {
output.push(suggestion);
}
}
return output;
}
}
} }
let mut add_candidate = |name, var_id: &VarId| { // Variable completion (e.g: $en<tab> to complete $env)
matcher.add_semantic_suggestion(SemanticSuggestion { for builtin in builtins {
suggestion: Suggestion { if options.match_algorithm.matches_u8_insensitive(
value: String::from_utf8_lossy(name).to_string(), options.case_sensitive,
span: current_span, builtin.as_bytes(),
description: Some(working_set.get_variable(*var_id).ty.to_string()), &prefix,
..Suggestion::default() ) {
}, output.push(SemanticSuggestion {
kind: Some(SuggestionKind::Variable), suggestion: Suggestion {
}) value: builtin.to_string(),
}; description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
// TODO is there a way to get the VarId to get the type???
kind: None,
});
}
}
// TODO: The following can be refactored (see find_commands_by_predicate() used in // TODO: The following can be refactored (see find_commands_by_predicate() used in
// command_completions). // command_completions).
@ -57,23 +185,190 @@ impl Completer for VariableCompletion {
// Working set scope vars // Working set scope vars
for scope_frame in working_set.delta.scope.iter().rev() { for scope_frame in working_set.delta.scope.iter().rev() {
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
for (name, var_id) in &overlay_frame.vars { for v in &overlay_frame.vars {
add_candidate(name, var_id); if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
v.0,
&prefix,
) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
)),
});
}
} }
} }
} }
// Permanent state vars // Permanent state vars
// for scope in &self.engine_state.scope { // for scope in &self.engine_state.scope {
for overlay_frame in working_set for overlay_frame in self.engine_state.active_overlays(&removed_overlays).rev() {
.permanent_state for v in &overlay_frame.vars {
.active_overlays(&removed_overlays) if options.match_algorithm.matches_u8_insensitive(
.rev() options.case_sensitive,
{ v.0,
for (name, var_id) in &overlay_frame.vars { &prefix,
add_candidate(name, var_id); ) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
)),
});
}
} }
} }
matcher.results() output.dedup(); // TODO: Removes only consecutive duplicates, is it intended?
output
}
}
// Find recursively the values for sublevels
// if no sublevels are set it returns the current value
fn nested_suggestions(
val: Value,
sublevels: Vec<Vec<u8>>,
current_span: reedline::Span,
) -> Vec<SemanticSuggestion> {
let mut output: Vec<SemanticSuggestion> = vec![];
let value = recursive_value(val, sublevels);
let kind = SuggestionKind::Type(value.get_type());
match value {
Value::Record { val, .. } => {
// Add all the columns as completion
for (col, _) in val.into_iter() {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: col,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(kind.clone()),
});
}
output
}
Value::LazyRecord { val, .. } => {
// Add all the columns as completion
for column_name in val.column_names() {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: column_name.to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(kind.clone()),
});
}
output
}
Value::List { vals, .. } => {
for column_name in get_columns(vals.as_slice()) {
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: column_name,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(kind.clone()),
});
}
output
}
_ => output,
}
}
// Extracts the recursive value (e.g: $var.a.b.c)
fn recursive_value(val: Value, sublevels: Vec<Vec<u8>>) -> Value {
// Go to next sublevel
if let Some(next_sublevel) = sublevels.clone().into_iter().next() {
let span = val.span();
match val {
Value::Record { val, .. } => {
for item in *val {
// Check if index matches with sublevel
if item.0.as_bytes().to_vec() == next_sublevel {
// If matches try to fetch recursively the next
return recursive_value(item.1, sublevels.into_iter().skip(1).collect());
}
}
// Current sublevel value not found
return Value::nothing(span);
}
Value::LazyRecord { val, .. } => {
for col in val.column_names() {
if col.as_bytes().to_vec() == next_sublevel {
return recursive_value(
val.get_column_value(col).unwrap_or_default(),
sublevels.into_iter().skip(1).collect(),
);
}
}
// Current sublevel value not found
return Value::nothing(span);
}
Value::List { vals, .. } => {
for col in get_columns(vals.as_slice()) {
if col.as_bytes().to_vec() == next_sublevel {
return recursive_value(
Value::list(vals, span)
.get_data_by_key(&col)
.unwrap_or_default(),
sublevels.into_iter().skip(1).collect(),
);
}
}
// Current sublevel value not found
return Value::nothing(span);
}
_ => return val,
}
}
val
}
impl MatchAlgorithm {
pub fn matches_u8_insensitive(&self, sensitive: bool, haystack: &[u8], needle: &[u8]) -> bool {
if sensitive {
self.matches_u8(haystack, needle)
} else {
self.matches_u8(&haystack.to_ascii_lowercase(), &needle.to_ascii_lowercase())
}
} }
} }

View File

@ -1,195 +1,84 @@
use crate::util::eval_source; use crate::util::eval_source;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
#[cfg(feature = "plugin")]
use nu_protocol::{engine::StateWorkingSet, ParseError, PluginRegistryFile, Spanned};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack, StateWorkingSet},
report_shell_error, PipelineData, report_error, HistoryFileFormat, PipelineData,
}; };
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
use nu_utils::perf; use nu_protocol::{ParseError, Spanned};
#[cfg(feature = "plugin")]
use nu_utils::utils::perf;
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
const PLUGIN_FILE: &str = "plugin.msgpackz"; const PLUGIN_FILE: &str = "plugin.nu";
#[cfg(feature = "plugin")]
const OLD_PLUGIN_FILE: &str = "plugin.nu"; const HISTORY_FILE_TXT: &str = "history.txt";
const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) { pub fn read_plugin_file(
use nu_protocol::{shell_error::io::IoError, ShellError}; engine_state: &mut EngineState,
use std::path::Path; stack: &mut Stack,
plugin_file: Option<Spanned<String>>,
storage_path: &str,
) {
let start_time = std::time::Instant::now();
let mut plug_path = String::new();
// Reading signatures from signature file
// The plugin.nu file stores the parsed signature collected from each registered plugin
add_plugin_file(engine_state, plugin_file, storage_path);
let span = plugin_file.as_ref().map(|s| s.span); let plugin_path = engine_state.plugin_signatures.clone();
// Check and warn + abort if this is a .nu plugin file
if plugin_file
.as_ref()
.and_then(|p| Path::new(&p.item).extension())
.is_some_and(|ext| ext == "nu")
{
report_shell_error(
engine_state,
&ShellError::GenericError {
error: "Wrong plugin file format".into(),
msg: ".nu plugin files are no longer supported".into(),
span,
help: Some("please recreate this file in the new .msgpackz format".into()),
inner: vec![],
},
);
return;
}
let mut start_time = std::time::Instant::now();
// Reading signatures from plugin registry file
// The plugin.msgpackz file stores the parsed signature collected from each registered plugin
add_plugin_file(engine_state, plugin_file.clone());
perf!(
"add plugin file to engine_state",
start_time,
engine_state
.get_config()
.use_ansi_coloring
.get(engine_state)
);
start_time = std::time::Instant::now();
let plugin_path = engine_state.plugin_path.clone();
if let Some(plugin_path) = plugin_path { if let Some(plugin_path) = plugin_path {
// Open the plugin file let plugin_filename = plugin_path.to_string_lossy();
let mut file = match std::fs::File::open(&plugin_path) { plug_path = plugin_filename.to_string();
Ok(file) => file, if let Ok(contents) = std::fs::read(&plugin_path) {
Err(err) => { eval_source(
if err.kind() == std::io::ErrorKind::NotFound { engine_state,
log::warn!("Plugin file not found: {}", plugin_path.display()); stack,
&contents,
// Try migration of an old plugin file if this wasn't a custom plugin file &plugin_filename,
if plugin_file.is_none() && migrate_old_plugin_file(engine_state) { PipelineData::empty(),
let Ok(file) = std::fs::File::open(&plugin_path) else { false,
log::warn!("Failed to load newly migrated plugin file");
return;
};
file
} else {
return;
}
} else {
report_shell_error(
engine_state,
&ShellError::Io(IoError::new_internal_with_path(
err.kind(),
"Could not open plugin registry file",
nu_protocol::location!(),
plugin_path,
)),
);
return;
}
}
};
// Abort if the file is empty.
if file.metadata().is_ok_and(|m| m.len() == 0) {
log::warn!(
"Not reading plugin file because it's empty: {}",
plugin_path.display()
); );
return;
} }
// Read the contents of the plugin file
let contents = match PluginRegistryFile::read_from(&mut file, span) {
Ok(contents) => contents,
Err(err) => {
log::warn!("Failed to read plugin registry file: {err:?}");
report_shell_error(
engine_state,
&ShellError::GenericError {
error: format!(
"Error while reading plugin registry file: {}",
plugin_path.display()
),
msg: "plugin path defined here".into(),
span,
help: Some(
"you might try deleting the file and registering all of your \
plugins again"
.into(),
),
inner: vec![],
},
);
return;
}
};
perf!(
&format!("read plugin file {}", plugin_path.display()),
start_time,
engine_state
.get_config()
.use_ansi_coloring
.get(engine_state)
);
start_time = std::time::Instant::now();
let mut working_set = StateWorkingSet::new(engine_state);
nu_plugin_engine::load_plugin_file(&mut working_set, &contents, span);
if let Err(err) = engine_state.merge_delta(working_set.render()) {
report_shell_error(engine_state, &err);
return;
}
perf!(
&format!("load plugin file {}", plugin_path.display()),
start_time,
engine_state
.get_config()
.use_ansi_coloring
.get(engine_state)
);
} }
perf(
&format!("read_plugin_file {}", &plug_path),
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
);
} }
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub fn add_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) { pub fn add_plugin_file(
use std::path::Path; engine_state: &mut EngineState,
plugin_file: Option<Spanned<String>>,
storage_path: &str,
) {
let working_set = StateWorkingSet::new(engine_state);
let cwd = working_set.get_cwd();
use nu_protocol::report_parse_error; if let Some(plugin_file) = plugin_file {
if let Ok(path) = canonicalize_with(&plugin_file.item, cwd) {
if let Ok(cwd) = engine_state.cwd_as_string(None) { engine_state.plugin_signatures = Some(path)
if let Some(plugin_file) = plugin_file { } else {
let path = Path::new(&plugin_file.item); let e = ParseError::FileNotFound(plugin_file.item, plugin_file.span);
let path_dir = path.parent().unwrap_or(path); report_error(&working_set, &e);
// Just try to canonicalize the directory of the plugin file first.
if let Ok(path_dir) = canonicalize_with(path_dir, &cwd) {
// Try to canonicalize the actual filename, but it's ok if that fails. The file doesn't
// have to exist.
let path = path_dir.join(path.file_name().unwrap_or(path.as_os_str()));
let path = canonicalize_with(&path, &cwd).unwrap_or(path);
engine_state.plugin_path = Some(path)
} else {
// It's an error if the directory for the plugin file doesn't exist.
report_parse_error(
&StateWorkingSet::new(engine_state),
&ParseError::FileNotFound(
path_dir.to_string_lossy().into_owned(),
plugin_file.span,
),
);
}
} else if let Some(plugin_path) = nu_path::nu_config_dir() {
// Path to store plugins signatures
let mut plugin_path =
canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path.into());
plugin_path.push(PLUGIN_FILE);
let plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
engine_state.plugin_path = Some(plugin_path);
} }
} else if let Some(mut plugin_path) = nu_path::config_dir() {
// Path to store plugins signatures
plugin_path.push(storage_path);
let mut plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
plugin_path.push(PLUGIN_FILE);
let plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
engine_state.plugin_signatures = Some(plugin_path);
} }
} }
@ -202,12 +91,7 @@ pub fn eval_config_contents(
let config_filename = config_path.to_string_lossy(); let config_filename = config_path.to_string_lossy();
if let Ok(contents) = std::fs::read(&config_path) { if let Ok(contents) = std::fs::read(&config_path) {
// Set the current active file to the config file. eval_source(
let prev_file = engine_state.file.take();
engine_state.file = Some(config_path.clone());
// TODO: ignore this error?
let _ = eval_source(
engine_state, engine_state,
stack, stack,
&contents, &contents,
@ -216,152 +100,30 @@ pub fn eval_config_contents(
false, false,
); );
// Restore the current active file.
engine_state.file = prev_file;
// Merge the environment in case env vars changed in the config // Merge the environment in case env vars changed in the config
if let Err(e) = engine_state.merge_env(stack) { match nu_engine::env::current_dir(engine_state, stack) {
report_shell_error(engine_state, &e); Ok(cwd) => {
if let Err(e) = engine_state.merge_env(stack, cwd) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
}
}
Err(e) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
}
} }
} }
} }
} }
#[cfg(feature = "plugin")] pub(crate) fn get_history_path(storage_path: &str, mode: HistoryFileFormat) -> Option<PathBuf> {
pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool { nu_path::config_dir().map(|mut history_path| {
use nu_protocol::{ history_path.push(storage_path);
shell_error::io::IoError, PluginExample, PluginIdentity, PluginRegistryItem, history_path.push(match mode {
PluginRegistryItemData, PluginSignature, ShellError, HistoryFileFormat::PlainText => HISTORY_FILE_TXT,
}; HistoryFileFormat::Sqlite => HISTORY_FILE_SQLITE,
use std::collections::BTreeMap;
let start_time = std::time::Instant::now();
let Ok(cwd) = engine_state.cwd_as_string(None) else {
return false;
};
let Some(config_dir) =
nu_path::nu_config_dir().and_then(|dir| nu_path::canonicalize_with(dir, &cwd).ok())
else {
return false;
};
let Ok(old_plugin_file_path) = nu_path::canonicalize_with(OLD_PLUGIN_FILE, &config_dir) else {
return false;
};
let old_contents = match std::fs::read(&old_plugin_file_path) {
Ok(old_contents) => old_contents,
Err(err) => {
report_shell_error(
engine_state,
&ShellError::GenericError {
error: "Can't read old plugin file to migrate".into(),
msg: "".into(),
span: None,
help: Some(err.to_string()),
inner: vec![],
},
);
return false;
}
};
// Make a copy of the engine state, because we'll read the newly generated file
let mut engine_state = engine_state.clone();
let mut stack = Stack::new();
if eval_source(
&mut engine_state,
&mut stack,
&old_contents,
&old_plugin_file_path.to_string_lossy(),
PipelineData::Empty,
false,
) != 0
{
return false;
}
// Now that the plugin commands are loaded, we just have to generate the file
let mut contents = PluginRegistryFile::new();
let mut groups = BTreeMap::<PluginIdentity, Vec<PluginSignature>>::new();
for decl in engine_state.plugin_decls() {
if let Some(identity) = decl.plugin_identity() {
groups
.entry(identity.clone())
.or_default()
.push(PluginSignature {
sig: decl.signature(),
examples: decl
.examples()
.into_iter()
.map(PluginExample::from)
.collect(),
})
}
}
for (identity, commands) in groups {
contents.upsert_plugin(PluginRegistryItem {
name: identity.name().to_owned(),
filename: identity.filename().to_owned(),
shell: identity.shell().map(|p| p.to_owned()),
data: PluginRegistryItemData::Valid {
metadata: Default::default(),
commands,
},
}); });
} history_path
})
// Write the new file
let new_plugin_file_path = config_dir.join(PLUGIN_FILE);
if let Err(err) = std::fs::File::create(&new_plugin_file_path)
.map_err(|err| {
IoError::new_internal_with_path(
err.kind(),
"Could not create new plugin file",
nu_protocol::location!(),
new_plugin_file_path.clone(),
)
})
.map_err(ShellError::from)
.and_then(|file| contents.write_to(file, None))
{
report_shell_error(
&engine_state,
&ShellError::GenericError {
error: "Failed to save migrated plugin file".into(),
msg: "".into(),
span: None,
help: Some("ensure `$nu.plugin-path` is writable".into()),
inner: vec![err],
},
);
return false;
}
if engine_state.is_interactive {
eprintln!(
"Your old plugin.nu file has been migrated to the new format: {}",
new_plugin_file_path.display()
);
eprintln!(
"The plugin.nu file has not been removed. If `plugin list` looks okay, \
you may do so manually."
);
}
perf!(
"migrate old plugin file",
start_time,
engine_state
.get_config()
.use_ansi_coloring
.get(&engine_state)
);
true
} }

View File

@ -1,22 +1,12 @@
use log::info; use log::info;
use nu_engine::eval_block; use miette::Result;
use nu_engine::{convert_env_values, eval_block};
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::{ use nu_protocol::{
cli_error::report_compile_error,
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
report_parse_error, report_parse_warning, PipelineData, ShellError, Spanned, Value, report_error, PipelineData, Spanned, Value,
}; };
use std::sync::Arc;
use crate::util::print_pipeline;
#[derive(Default)]
pub struct EvaluateCommandsOpts {
pub table_mode: Option<Value>,
pub error_style: Option<Value>,
pub no_newline: bool,
}
/// Run a command (or commands) given to us by the user /// Run a command (or commands) given to us by the user
pub fn evaluate_commands( pub fn evaluate_commands(
@ -24,53 +14,33 @@ pub fn evaluate_commands(
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
input: PipelineData, input: PipelineData,
opts: EvaluateCommandsOpts, table_mode: Option<Value>,
) -> Result<(), ShellError> { ) -> Result<Option<i64>> {
let EvaluateCommandsOpts { // Translate environment variables from Strings to Values
table_mode, if let Some(e) = convert_env_values(engine_state, stack) {
error_style, let working_set = StateWorkingSet::new(engine_state);
no_newline, report_error(&working_set, &e);
} = opts; std::process::exit(1);
// Handle the configured error style early
if let Some(e_style) = error_style {
match e_style.coerce_str()?.parse() {
Ok(e_style) => {
Arc::make_mut(&mut engine_state.config).error_style = e_style;
}
Err(err) => {
return Err(ShellError::GenericError {
error: "Invalid value for `--error-style`".into(),
msg: err.into(),
span: Some(e_style.span()),
help: None,
inner: vec![],
});
}
}
} }
// Parse the source code // Parse the source code
let (block, delta) = { let (block, delta) = {
if let Some(ref t_mode) = table_mode { if let Some(ref t_mode) = table_mode {
Arc::make_mut(&mut engine_state.config).table.mode = let mut config = engine_state.get_config().clone();
t_mode.coerce_str()?.parse().unwrap_or_default(); config.table_mode = t_mode.coerce_str()?.parse().unwrap_or_default();
engine_state.set_config(config);
} }
let mut working_set = StateWorkingSet::new(engine_state); let mut working_set = StateWorkingSet::new(engine_state);
let output = parse(&mut working_set, None, commands.item.as_bytes(), false); let output = parse(&mut working_set, None, commands.item.as_bytes(), false);
if let Some(warning) = working_set.parse_warnings.first() { if let Some(warning) = working_set.parse_warnings.first() {
report_parse_warning(&working_set, warning); report_error(&working_set, warning);
} }
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
report_parse_error(&working_set, err); report_error(&working_set, err);
std::process::exit(1);
}
if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err);
std::process::exit(1); std::process::exit(1);
} }
@ -78,23 +48,29 @@ pub fn evaluate_commands(
}; };
// Update permanent state // Update permanent state
engine_state.merge_delta(delta)?; if let Err(err) = engine_state.merge_delta(delta) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
}
// Run the block // Run the block
let pipeline = eval_block::<WithoutDebug>(engine_state, stack, &block, input)?; let exit_code = match eval_block::<WithoutDebug>(engine_state, stack, &block, input) {
Ok(pipeline_data) => {
let mut config = engine_state.get_config().clone();
if let Some(t_mode) = table_mode {
config.table_mode = t_mode.coerce_str()?.parse().unwrap_or_default();
}
crate::eval_file::print_table_or_error(engine_state, stack, pipeline_data, &mut config)
}
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);
if let PipelineData::Value(Value::Error { error, .. }, ..) = pipeline { report_error(&working_set, &err);
return Err(*error); std::process::exit(1);
} }
};
if let Some(t_mode) = table_mode {
Arc::make_mut(&mut engine_state.config).table.mode =
t_mode.coerce_str()?.parse().unwrap_or_default();
}
print_pipeline(engine_state, stack, pipeline, no_newline)?;
info!("evaluate {}:{}:{}", file!(), line!(), column!()); info!("evaluate {}:{}:{}", file!(), line!(), column!());
Ok(()) Ok(exit_code)
} }

View File

@ -1,68 +1,93 @@
use crate::util::{eval_source, print_pipeline}; use crate::util::eval_source;
use log::{info, trace}; use log::{info, trace};
use nu_engine::eval_block; use miette::{IntoDiagnostic, Result};
use nu_engine::{convert_env_values, current_dir, eval_block};
use nu_parser::parse; use nu_parser::parse;
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
use nu_protocol::{ use nu_protocol::{
cli_error::report_compile_error, ast::Call,
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
report_parse_error, report_parse_warning, report_error, Config, PipelineData, ShellError, Span, Value,
shell_error::io::*,
PipelineData, ShellError, Span, Value,
}; };
use std::{path::PathBuf, sync::Arc}; use nu_utils::stdout_write_all_and_flush;
use std::sync::Arc;
/// Entry point for evaluating a file. /// Main function used when a file path is found as argument for nu
///
/// If the file contains a main command, it is invoked with `args` and the pipeline data from `input`;
/// otherwise, the pipeline data is forwarded to the first command in the file, and `args` are ignored.
pub fn evaluate_file( pub fn evaluate_file(
path: String, path: String,
args: &[String], args: &[String],
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
input: PipelineData, input: PipelineData,
) -> Result<(), ShellError> { ) -> Result<()> {
let cwd = engine_state.cwd_as_string(Some(stack))?; // Translate environment variables from Strings to Values
if let Some(e) = convert_env_values(engine_state, stack) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
std::process::exit(1);
}
let file_path = canonicalize_with(&path, cwd).map_err(|err| { let cwd = current_dir(engine_state, stack)?;
IoError::new_internal_with_path(
err.kind().not_found_as(NotFound::File),
"Could not access file",
nu_protocol::location!(),
PathBuf::from(&path),
)
})?;
let file_path_str = file_path let file_path = canonicalize_with(&path, cwd).unwrap_or_else(|e| {
.to_str() let working_set = StateWorkingSet::new(engine_state);
.ok_or_else(|| ShellError::NonUtf8Custom { report_error(
msg: format!( &working_set,
"Input file name '{}' is not valid UTF8", &ShellError::FileNotFoundCustom {
file_path.to_string_lossy() msg: format!("Could not access file '{}': {:?}", path, e.to_string()),
), span: Span::unknown(),
span: Span::unknown(), },
})?; );
std::process::exit(1);
});
let file = std::fs::read(&file_path).map_err(|err| { let file_path_str = file_path.to_str().unwrap_or_else(|| {
IoError::new_internal_with_path( let working_set = StateWorkingSet::new(engine_state);
err.kind().not_found_as(NotFound::File), report_error(
"Could not read file", &working_set,
nu_protocol::location!(), &ShellError::NonUtf8Custom {
file_path.clone(), msg: format!(
) "Input file name '{}' is not valid UTF8",
})?; file_path.to_string_lossy()
engine_state.file = Some(file_path.clone()); ),
span: Span::unknown(),
},
);
std::process::exit(1);
});
let parent = file_path.parent().ok_or_else(|| { let file = std::fs::read(&file_path)
IoError::new_internal_with_path( .into_diagnostic()
ErrorKind::DirectoryNotFound, .unwrap_or_else(|e| {
"The file path does not have a parent", let working_set = StateWorkingSet::new(engine_state);
nu_protocol::location!(), report_error(
file_path.clone(), &working_set,
) &ShellError::FileNotFoundCustom {
})?; msg: format!(
"Could not read file '{}': {:?}",
file_path_str,
e.to_string()
),
span: Span::unknown(),
},
);
std::process::exit(1);
});
engine_state.start_in_file(Some(file_path_str));
let parent = file_path.parent().unwrap_or_else(|| {
let working_set = StateWorkingSet::new(engine_state);
report_error(
&working_set,
&ShellError::FileNotFoundCustom {
msg: format!("The file path '{file_path_str}' does not have a parent"),
span: Span::unknown(),
},
);
std::process::exit(1);
});
stack.add_env_var( stack.add_env_var(
"FILE_PWD".to_string(), "FILE_PWD".to_string(),
@ -79,28 +104,17 @@ pub fn evaluate_file(
let source_filename = file_path let source_filename = file_path
.file_name() .file_name()
.expect("internal error: missing filename"); .expect("internal error: script missing filename");
let mut working_set = StateWorkingSet::new(engine_state); let mut working_set = StateWorkingSet::new(engine_state);
trace!("parsing file: {}", file_path_str); trace!("parsing file: {}", file_path_str);
let block = parse(&mut working_set, Some(file_path_str), &file, false); let block = parse(&mut working_set, Some(file_path_str), &file, false);
if let Some(warning) = working_set.parse_warnings.first() {
report_parse_warning(&working_set, warning);
}
// If any parse errors were found, report the first error and exit.
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
report_parse_error(&working_set, err); report_error(&working_set, err);
std::process::exit(1); std::process::exit(1);
} }
if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err);
std::process::exit(1);
}
// Look for blocks whose name starts with "main" and replace it with the filename.
for block in working_set.delta.blocks.iter_mut().map(Arc::make_mut) { for block in working_set.delta.blocks.iter_mut().map(Arc::make_mut) {
if block.signature.name == "main" { if block.signature.name == "main" {
block.signature.name = source_filename.to_string_lossy().to_string(); block.signature.name = source_filename.to_string_lossy().to_string();
@ -110,45 +124,131 @@ pub fn evaluate_file(
} }
} }
// Merge the changes into the engine state. let _ = engine_state.merge_delta(working_set.delta);
engine_state.merge_delta(working_set.delta)?;
// Check if the file contains a main command. if engine_state.find_decl(b"main", &[]).is_some() {
let exit_code = if engine_state.find_decl(b"main", &[]).is_some() {
// Evaluate the file, but don't run main yet.
let pipeline =
match eval_block::<WithoutDebug>(engine_state, stack, &block, PipelineData::empty()) {
Ok(data) => data,
Err(ShellError::Return { .. }) => {
// Allow early return before main is run.
return Ok(());
}
Err(err) => return Err(err),
};
// Print the pipeline output of the last command of the file.
print_pipeline(engine_state, stack, pipeline, true)?;
// Invoke the main command with arguments.
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
let args = format!("main {}", args.join(" ")); let args = format!("main {}", args.join(" "));
eval_source(
let pipeline_data =
eval_block::<WithoutDebug>(engine_state, stack, &block, PipelineData::empty());
let pipeline_data = match pipeline_data {
Err(ShellError::Return { .. }) => {
// allows early exists before `main` is run.
return Ok(());
}
x => x,
}
.unwrap_or_else(|e| {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
std::process::exit(1);
});
let result = pipeline_data.print(engine_state, stack, true, false);
match result {
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
std::process::exit(1);
}
Ok(exit_code) => {
if exit_code != 0 {
std::process::exit(exit_code as i32);
}
}
}
if !eval_source(
engine_state, engine_state,
stack, stack,
args.as_bytes(), args.as_bytes(),
"<commandline>", "<commandline>",
input, input,
true, true,
) ) {
} else { std::process::exit(1);
eval_source(engine_state, stack, &file, file_path_str, input, true) }
}; } else if !eval_source(engine_state, stack, &file, file_path_str, input, true) {
std::process::exit(1);
if exit_code != 0 {
std::process::exit(exit_code);
} }
info!("evaluate {}:{}:{}", file!(), line!(), column!()); info!("evaluate {}:{}:{}", file!(), line!(), column!());
Ok(()) Ok(())
} }
pub(crate) fn print_table_or_error(
engine_state: &mut EngineState,
stack: &mut Stack,
mut pipeline_data: PipelineData,
config: &mut Config,
) -> Option<i64> {
let exit_code = match &mut pipeline_data {
PipelineData::ExternalStream { exit_code, .. } => exit_code.take(),
_ => None,
};
// Change the engine_state config to use the passed in configuration
engine_state.set_config(config.clone());
if let PipelineData::Value(Value::Error { error, .. }, ..) = &pipeline_data {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &**error);
std::process::exit(1);
}
if let Some(decl_id) = engine_state.find_decl("table".as_bytes(), &[]) {
let command = engine_state.get_decl(decl_id);
if command.get_block_id().is_some() {
print_or_exit(pipeline_data, engine_state, config);
} else {
// The final call on table command, it's ok to set redirect_output to false.
let call = Call::new(Span::new(0, 0));
let table = command.run(engine_state, stack, &call, pipeline_data);
match table {
Ok(table) => {
print_or_exit(table, engine_state, config);
}
Err(error) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &error);
std::process::exit(1);
}
}
}
} else {
print_or_exit(pipeline_data, engine_state, config);
}
// Make sure everything has finished
if let Some(exit_code) = exit_code {
let mut exit_code: Vec<_> = exit_code.into_iter().collect();
exit_code
.pop()
.and_then(|last_exit_code| match last_exit_code {
Value::Int { val: code, .. } => Some(code),
_ => None,
})
} else {
None
}
}
fn print_or_exit(pipeline_data: PipelineData, engine_state: &mut EngineState, config: &Config) {
for item in pipeline_data {
if let Value::Error { error, .. } = item {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &*error);
std::process::exit(1);
}
let out = item.to_expanded_string("\n", config) + "\n";
let _ = stdout_write_all_and_flush(out).map_err(|err| eprintln!("{err}"));
}
}

View File

@ -1,4 +1,3 @@
#![doc = include_str!("../README.md")]
mod commands; mod commands;
mod completions; mod completions;
mod config_files; mod config_files;
@ -18,9 +17,10 @@ mod validation;
pub use commands::add_cli_context; pub use commands::add_cli_context;
pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind}; pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind};
pub use config_files::eval_config_contents; pub use config_files::eval_config_contents;
pub use eval_cmds::{evaluate_commands, EvaluateCommandsOpts}; pub use eval_cmds::evaluate_commands;
pub use eval_file::evaluate_file; pub use eval_file::evaluate_file;
pub use menus::NuHelpCompleter; pub use menus::NuHelpCompleter;
pub use nu_cmd_base::util::get_init_cwd;
pub use nu_highlight::NuHighlight; pub use nu_highlight::NuHighlight;
pub use print::Print; pub use print::Print;
pub use prompt::NushellPrompt; pub use prompt::NushellPrompt;
@ -32,6 +32,4 @@ pub use validation::NuValidator;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub use config_files::add_plugin_file; pub use config_files::add_plugin_file;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub use config_files::migrate_old_plugin_file;
#[cfg(feature = "plugin")]
pub use config_files::read_plugin_file; pub use config_files::read_plugin_file;

View File

@ -1,73 +1,62 @@
use nu_engine::documentation::{get_flags_section, HelpStyle}; use nu_engine::documentation::get_flags_section;
use nu_protocol::{engine::EngineState, levenshtein_distance, Config}; use nu_protocol::{engine::EngineState, levenshtein_distance};
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use reedline::{Completer, Suggestion}; use reedline::{Completer, Suggestion};
use std::{fmt::Write, sync::Arc}; use std::{fmt::Write, sync::Arc};
pub struct NuHelpCompleter { pub struct NuHelpCompleter(Arc<EngineState>);
engine_state: Arc<EngineState>,
config: Arc<Config>,
}
impl NuHelpCompleter { impl NuHelpCompleter {
pub fn new(engine_state: Arc<EngineState>, config: Arc<Config>) -> Self { pub fn new(engine_state: Arc<EngineState>) -> Self {
Self { Self(engine_state)
engine_state,
config,
}
} }
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> { fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
let full_commands = self.0.get_signatures_with_examples(false);
let folded_line = line.to_folded_case(); let folded_line = line.to_folded_case();
let mut help_style = HelpStyle::default(); //Vec<(Signature, Vec<Example>, bool, bool)> {
help_style.update_from_config(&self.engine_state, &self.config); let mut commands = full_commands
.iter()
let mut commands = self .filter(|(sig, _, _, _, _)| {
.engine_state sig.name.to_folded_case().contains(&folded_line)
.get_decls_sorted(false) || sig.usage.to_folded_case().contains(&folded_line)
.into_iter() || sig
.filter_map(|(_, decl_id)| { .search_terms
let decl = self.engine_state.get_decl(decl_id); .iter()
(decl.name().to_folded_case().contains(&folded_line)
|| decl.description().to_folded_case().contains(&folded_line)
|| decl
.search_terms()
.into_iter()
.any(|term| term.to_folded_case().contains(&folded_line)) .any(|term| term.to_folded_case().contains(&folded_line))
|| decl || sig.extra_usage.to_folded_case().contains(&folded_line)
.extra_description()
.to_folded_case()
.contains(&folded_line))
.then_some(decl)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
commands.sort_by_cached_key(|decl| levenshtein_distance(line, decl.name())); commands.sort_by(|(a, _, _, _, _), (b, _, _, _, _)| {
let a_distance = levenshtein_distance(line, &a.name);
let b_distance = levenshtein_distance(line, &b.name);
a_distance.cmp(&b_distance)
});
commands commands
.into_iter() .into_iter()
.map(|decl| { .map(|(sig, examples, _, _, _)| {
let mut long_desc = String::new(); let mut long_desc = String::new();
let description = decl.description(); let usage = &sig.usage;
if !description.is_empty() { if !usage.is_empty() {
long_desc.push_str(description); long_desc.push_str(usage);
long_desc.push_str("\r\n\r\n"); long_desc.push_str("\r\n\r\n");
} }
let extra_desc = decl.extra_description(); let extra_usage = &sig.extra_usage;
if !extra_desc.is_empty() { if !extra_usage.is_empty() {
long_desc.push_str(extra_desc); long_desc.push_str(extra_usage);
long_desc.push_str("\r\n\r\n"); long_desc.push_str("\r\n\r\n");
} }
let sig = decl.signature();
let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature()); let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature());
if !sig.named.is_empty() { if !sig.named.is_empty() {
long_desc.push_str(&get_flags_section(&sig, &help_style, |v| { long_desc.push_str(&get_flags_section(Some(&*self.0.clone()), sig, |v| {
v.to_parsable_string(", ", &self.config) v.to_parsable_string(", ", &self.0.config)
})) }))
} }
@ -83,7 +72,7 @@ impl NuHelpCompleter {
let opt_suffix = if let Some(value) = &positional.default_value { let opt_suffix = if let Some(value) = &positional.default_value {
format!( format!(
" (optional, default: {})", " (optional, default: {})",
&value.to_parsable_string(", ", &self.config), &value.to_parsable_string(", ", &self.0.config),
) )
} else { } else {
(" (optional)").to_string() (" (optional)").to_string()
@ -104,21 +93,21 @@ impl NuHelpCompleter {
} }
} }
let extra: Vec<String> = decl let extra: Vec<String> = examples
.examples()
.iter() .iter()
.map(|example| example.example.replace('\n', "\r\n")) .map(|example| example.example.replace('\n', "\r\n"))
.collect(); .collect();
Suggestion { Suggestion {
value: decl.name().into(), value: sig.name.clone(),
description: Some(long_desc), description: Some(long_desc),
style: None,
extra: Some(extra), extra: Some(extra),
span: reedline::Span { span: reedline::Span {
start: pos - line.len(), start: pos - line.len(),
end: pos, end: pos,
}, },
..Suggestion::default() append_whitespace: false,
} }
}) })
.collect() .collect()
@ -149,8 +138,7 @@ mod test {
) { ) {
let engine_state = let engine_state =
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()); nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
let config = engine_state.get_config().clone(); let mut completer = NuHelpCompleter::new(engine_state.into());
let mut completer = NuHelpCompleter::new(engine_state.into(), config);
let suggestions = completer.complete(line, end); let suggestions = completer.complete(line, end);
assert_eq!( assert_eq!(

View File

@ -2,7 +2,7 @@ use nu_engine::eval_block;
use nu_protocol::{ use nu_protocol::{
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack}, engine::{EngineState, Stack},
BlockId, IntoPipelineData, Span, Value, IntoPipelineData, Span, Value,
}; };
use reedline::{menu_functions::parse_selection_char, Completer, Suggestion}; use reedline::{menu_functions::parse_selection_char, Completer, Suggestion};
use std::sync::Arc; use std::sync::Arc;
@ -10,7 +10,7 @@ use std::sync::Arc;
const SELECTION_CHAR: char = '!'; const SELECTION_CHAR: char = '!';
pub struct NuMenuCompleter { pub struct NuMenuCompleter {
block_id: BlockId, block_id: usize,
span: Span, span: Span,
stack: Stack, stack: Stack,
engine_state: Arc<EngineState>, engine_state: Arc<EngineState>,
@ -19,7 +19,7 @@ pub struct NuMenuCompleter {
impl NuMenuCompleter { impl NuMenuCompleter {
pub fn new( pub fn new(
block_id: BlockId, block_id: usize,
span: Span, span: Span,
stack: Stack, stack: Stack,
engine_state: Arc<EngineState>, engine_state: Arc<EngineState>,
@ -28,7 +28,7 @@ impl NuMenuCompleter {
Self { Self {
block_id, block_id,
span, span,
stack: stack.reset_out_dest().collect_value(), stack: stack.reset_stdio().capture(),
engine_state, engine_state,
only_buffer_difference, only_buffer_difference,
} }
@ -59,7 +59,8 @@ impl Completer for NuMenuCompleter {
let res = eval_block::<WithoutDebug>(&self.engine_state, &mut self.stack, block, input); let res = eval_block::<WithoutDebug>(&self.engine_state, &mut self.stack, block, input);
if let Ok(values) = res.and_then(|data| data.into_value(self.span)) { if let Ok(values) = res {
let values = values.into_value(self.span);
convert_to_suggestions(values, line, pos, self.only_buffer_difference) convert_to_suggestions(values, line, pos, self.only_buffer_difference)
} else { } else {
Vec::new() Vec::new()
@ -142,9 +143,10 @@ fn convert_to_suggestions(
vec![Suggestion { vec![Suggestion {
value: text, value: text,
description, description,
style: None,
extra, extra,
span, span,
..Suggestion::default() append_whitespace: false,
}] }]
} }
Value::List { vals, .. } => vals Value::List { vals, .. } => vals
@ -153,6 +155,9 @@ fn convert_to_suggestions(
.collect(), .collect(),
_ => vec![Suggestion { _ => vec![Suggestion {
value: format!("Not a record: {value:?}"), value: format!("Not a record: {value:?}"),
description: None,
style: None,
extra: None,
span: reedline::Span { span: reedline::Span {
start: if only_buffer_difference { start: if only_buffer_difference {
pos - line.len() pos - line.len()
@ -165,7 +170,7 @@ fn convert_to_suggestions(
line.len() line.len()
}, },
}, },
..Suggestion::default() append_whitespace: false,
}], }],
} }
} }

View File

@ -1,5 +1,3 @@
use std::sync::Arc;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use reedline::{Highlighter, StyledText}; use reedline::{Highlighter, StyledText};
@ -17,7 +15,7 @@ impl Command for NuHighlight {
.input_output_types(vec![(Type::String, Type::String)]) .input_output_types(vec![(Type::String, Type::String)])
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Syntax highlight the input string." "Syntax highlight the input string."
} }
@ -34,11 +32,14 @@ impl Command for NuHighlight {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let signals = engine_state.signals(); let ctrlc = engine_state.ctrlc.clone();
let engine_state = std::sync::Arc::new(engine_state.clone());
let config = engine_state.get_config().clone();
let highlighter = crate::NuHighlighter { let highlighter = crate::NuHighlighter {
engine_state: Arc::new(engine_state.clone()), engine_state,
stack: Arc::new(stack.clone()), stack: std::sync::Arc::new(stack.clone()),
config,
}; };
input.map( input.map(
@ -49,7 +50,7 @@ impl Command for NuHighlight {
} }
Err(err) => Value::error(err, head), Err(err) => Value::error(err, head),
}, },
signals, ctrlc,
) )
} }

View File

@ -1,5 +1,4 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::ByteStreamSource;
#[derive(Clone)] #[derive(Clone)]
pub struct Print; pub struct Print;
@ -23,19 +22,14 @@ impl Command for Print {
Some('n'), Some('n'),
) )
.switch("stderr", "print to stderr instead of stdout", Some('e')) .switch("stderr", "print to stderr instead of stdout", Some('e'))
.switch(
"raw",
"print without formatting (including binary data)",
Some('r'),
)
.category(Category::Strings) .category(Category::Strings)
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Print the given values to stdout." "Print the given values to stdout."
} }
fn extra_description(&self) -> &str { fn extra_usage(&self) -> &str {
r#"Unlike `echo`, this command does not return any value (`print | describe` will return "nothing"). r#"Unlike `echo`, this command does not return any value (`print | describe` will return "nothing").
Since this command has no output, there is no point in piping it with other commands. Since this command has no output, there is no point in piping it with other commands.
@ -51,39 +45,20 @@ Since this command has no output, there is no point in piping it with other comm
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
call: &Call, call: &Call,
mut input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let args: Vec<Value> = call.rest(engine_state, stack, 0)?; let args: Vec<Value> = call.rest(engine_state, stack, 0)?;
let no_newline = call.has_flag(engine_state, stack, "no-newline")?; let no_newline = call.has_flag(engine_state, stack, "no-newline")?;
let to_stderr = call.has_flag(engine_state, stack, "stderr")?; let to_stderr = call.has_flag(engine_state, stack, "stderr")?;
let raw = call.has_flag(engine_state, stack, "raw")?;
// This will allow for easy printing of pipelines as well // This will allow for easy printing of pipelines as well
if !args.is_empty() { if !args.is_empty() {
for arg in args { for arg in args {
if raw { arg.into_pipeline_data()
arg.into_pipeline_data() .print(engine_state, stack, no_newline, to_stderr)?;
.print_raw(engine_state, no_newline, to_stderr)?;
} else {
arg.into_pipeline_data().print_table(
engine_state,
stack,
no_newline,
to_stderr,
)?;
}
} }
} else if !input.is_nothing() { } else if !input.is_nothing() {
if let PipelineData::ByteStream(stream, _) = &mut input { input.print(engine_state, stack, no_newline, to_stderr)?;
if let ByteStreamSource::Child(child) = stream.source_mut() {
child.ignore_error(true);
}
}
if raw {
input.print_raw(engine_state, no_newline, to_stderr)?;
} else {
input.print_table(engine_state, stack, no_newline, to_stderr)?;
}
} }
Ok(PipelineData::empty()) Ok(PipelineData::empty())
@ -101,11 +76,6 @@ Since this command has no output, there is no point in piping it with other comm
example: r#"print (2 + 3)"#, example: r#"print (2 + 3)"#,
result: None, result: None,
}, },
Example {
description: "Print 'ABC' from binary data",
example: r#"0x[41 42 43] | print --raw"#,
result: None,
},
] ]
} }
} }

View File

@ -1,7 +1,4 @@
use crate::prompt_update::{ use crate::prompt_update::{POST_PROMPT_MARKER, PRE_PROMPT_MARKER};
POST_PROMPT_MARKER, PRE_PROMPT_MARKER, VSCODE_POST_PROMPT_MARKER, VSCODE_PRE_PROMPT_MARKER,
};
use nu_protocol::engine::{EngineState, Stack};
#[cfg(windows)] #[cfg(windows)]
use nu_utils::enable_vt_processing; use nu_utils::enable_vt_processing;
use reedline::{ use reedline::{
@ -13,8 +10,7 @@ use std::borrow::Cow;
/// Nushell prompt definition /// Nushell prompt definition
#[derive(Clone)] #[derive(Clone)]
pub struct NushellPrompt { pub struct NushellPrompt {
shell_integration_osc133: bool, shell_integration: bool,
shell_integration_osc633: bool,
left_prompt_string: Option<String>, left_prompt_string: Option<String>,
right_prompt_string: Option<String>, right_prompt_string: Option<String>,
default_prompt_indicator: Option<String>, default_prompt_indicator: Option<String>,
@ -22,20 +18,12 @@ pub struct NushellPrompt {
default_vi_normal_prompt_indicator: Option<String>, default_vi_normal_prompt_indicator: Option<String>,
default_multiline_indicator: Option<String>, default_multiline_indicator: Option<String>,
render_right_prompt_on_last_line: bool, render_right_prompt_on_last_line: bool,
engine_state: EngineState,
stack: Stack,
} }
impl NushellPrompt { impl NushellPrompt {
pub fn new( pub fn new(shell_integration: bool) -> NushellPrompt {
shell_integration_osc133: bool,
shell_integration_osc633: bool,
engine_state: EngineState,
stack: Stack,
) -> NushellPrompt {
NushellPrompt { NushellPrompt {
shell_integration_osc133, shell_integration,
shell_integration_osc633,
left_prompt_string: None, left_prompt_string: None,
right_prompt_string: None, right_prompt_string: None,
default_prompt_indicator: None, default_prompt_indicator: None,
@ -43,8 +31,6 @@ impl NushellPrompt {
default_vi_normal_prompt_indicator: None, default_vi_normal_prompt_indicator: None,
default_multiline_indicator: None, default_multiline_indicator: None,
render_right_prompt_on_last_line: false, render_right_prompt_on_last_line: false,
engine_state,
stack,
} }
} }
@ -120,22 +106,7 @@ impl Prompt for NushellPrompt {
.to_string() .to_string()
.replace('\n', "\r\n"); .replace('\n', "\r\n");
if self.shell_integration_osc633 { if self.shell_integration {
if self
.stack
.get_env_var(&self.engine_state, "TERM_PROGRAM")
.and_then(|v| v.as_str().ok())
== Some("vscode")
{
// We're in vscode and we have osc633 enabled
format!("{VSCODE_PRE_PROMPT_MARKER}{prompt}{VSCODE_POST_PROMPT_MARKER}").into()
} else if self.shell_integration_osc133 {
// If we're in VSCode but we don't find the env var, but we have osc133 set, then use it
format!("{PRE_PROMPT_MARKER}{prompt}{POST_PROMPT_MARKER}").into()
} else {
prompt.into()
}
} else if self.shell_integration_osc133 {
format!("{PRE_PROMPT_MARKER}{prompt}{POST_PROMPT_MARKER}").into() format!("{PRE_PROMPT_MARKER}{prompt}{POST_PROMPT_MARKER}").into()
} else { } else {
prompt.into() prompt.into()

View File

@ -1,9 +1,9 @@
use crate::NushellPrompt; use crate::NushellPrompt;
use log::{trace, warn}; use log::trace;
use nu_engine::ClosureEvalOnce; use nu_engine::get_eval_subexpression;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack, StateWorkingSet},
report_shell_error, Config, PipelineData, Value, report_error, Config, PipelineData, Value,
}; };
use reedline::Prompt; use reedline::Prompt;
@ -23,31 +23,10 @@ pub(crate) const TRANSIENT_PROMPT_INDICATOR_VI_NORMAL: &str =
"TRANSIENT_PROMPT_INDICATOR_VI_NORMAL"; "TRANSIENT_PROMPT_INDICATOR_VI_NORMAL";
pub(crate) const TRANSIENT_PROMPT_MULTILINE_INDICATOR: &str = pub(crate) const TRANSIENT_PROMPT_MULTILINE_INDICATOR: &str =
"TRANSIENT_PROMPT_MULTILINE_INDICATOR"; "TRANSIENT_PROMPT_MULTILINE_INDICATOR";
// Store all these Ansi Escape Markers here so they can be reused easily
// According to Daniel Imms @Tyriar, we need to do these this way: // According to Daniel Imms @Tyriar, we need to do these this way:
// <133 A><prompt><133 B><command><133 C><command output> // <133 A><prompt><133 B><command><133 C><command output>
pub(crate) const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\"; pub(crate) const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\";
pub(crate) const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\"; pub(crate) const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\";
pub(crate) const PRE_EXECUTION_MARKER: &str = "\x1b]133;C\x1b\\";
pub(crate) const POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]133;D;";
pub(crate) const POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
// OSC633 is the same as OSC133 but specifically for VSCode
pub(crate) const VSCODE_PRE_PROMPT_MARKER: &str = "\x1b]633;A\x1b\\";
pub(crate) const VSCODE_POST_PROMPT_MARKER: &str = "\x1b]633;B\x1b\\";
pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
//"\x1b]633;D;{}\x1b\\"
pub(crate) const VSCODE_POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]633;D;";
pub(crate) const VSCODE_POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
//"\x1b]633;E;{}\x1b\\"
pub(crate) const VSCODE_COMMANDLINE_MARKER_PREFIX: &str = "\x1b]633;E;";
pub(crate) const VSCODE_COMMANDLINE_MARKER_SUFFIX: &str = "\x1b\\";
// "\x1b]633;P;Cwd={}\x1b\\"
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_PREFIX: &str = "\x1b]633;P;Cwd=";
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_SUFFIX: &str = "\x1b\\";
pub(crate) const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
fn get_prompt_string( fn get_prompt_string(
prompt: &str, prompt: &str,
@ -55,13 +34,17 @@ fn get_prompt_string(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
) -> Option<String> { ) -> Option<String> {
let eval_subexpression = get_eval_subexpression(engine_state);
stack stack
.get_env_var(engine_state, prompt) .get_env_var(engine_state, prompt)
.and_then(|v| match v { .and_then(|v| match v {
Value::Closure { val, .. } => { Value::Closure { val, .. } => {
let result = ClosureEvalOnce::new(engine_state, stack, val.as_ref().clone()) let block = engine_state.get_block(val.block_id);
.run_with_input(PipelineData::Empty); let mut stack = stack.captures_to_stack(val.captures);
// Use eval_subexpression to force a redirection of output, so we can use everything in prompt
let ret_val =
eval_subexpression(engine_state, &mut stack, block, PipelineData::empty());
trace!( trace!(
"get_prompt_string (block) {}:{}:{}", "get_prompt_string (block) {}:{}:{}",
file!(), file!(),
@ -69,9 +52,28 @@ fn get_prompt_string(
column!() column!()
); );
result ret_val
.map_err(|err| { .map_err(|err| {
report_shell_error(engine_state, &err); let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
})
.ok()
}
Value::Block { val: block_id, .. } => {
let block = engine_state.get_block(block_id);
// Use eval_subexpression to force a redirection of output, so we can use everything in prompt
let ret_val = eval_subexpression(engine_state, stack, block, PipelineData::empty());
trace!(
"get_prompt_string (block) {}:{}:{}",
file!(),
line!(),
column!()
);
ret_val
.map_err(|err| {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
}) })
.ok() .ok()
} }
@ -80,19 +82,18 @@ fn get_prompt_string(
}) })
.and_then(|pipeline_data| { .and_then(|pipeline_data| {
let output = pipeline_data.collect_string("", config).ok(); let output = pipeline_data.collect_string("", config).ok();
let ansi_output = output.map(|mut x| {
// Always reset the color at the start of the right prompt
// to ensure there is no ansi bleed over
if x.is_empty() && prompt == PROMPT_COMMAND_RIGHT {
x.insert_str(0, "\x1b[0m")
};
output.map(|mut x| {
// Just remove the very last newline.
if x.ends_with('\n') {
x.pop();
}
if x.ends_with('\r') {
x.pop();
}
x x
}); })
// Let's keep this for debugging purposes with nu --log-level warn
warn!("{}:{}:{} {:?}", file!(), line!(), column!(), ansi_output);
ansi_output
}) })
} }
@ -102,38 +103,20 @@ pub(crate) fn update_prompt(
stack: &mut Stack, stack: &mut Stack,
nu_prompt: &mut NushellPrompt, nu_prompt: &mut NushellPrompt,
) { ) {
let configured_left_prompt_string = let left_prompt_string = get_prompt_string(PROMPT_COMMAND, config, engine_state, stack);
match get_prompt_string(PROMPT_COMMAND, config, engine_state, stack) {
Some(s) => s,
None => "".to_string(),
};
// Now that we have the prompt string lets ansify it. // Now that we have the prompt string lets ansify it.
// <133 A><prompt><133 B><command><133 C><command output> // <133 A><prompt><133 B><command><133 C><command output>
let left_prompt_string = if config.shell_integration.osc633 { let left_prompt_string = if config.shell_integration {
if stack if let Some(prompt_string) = left_prompt_string {
.get_env_var(engine_state, "TERM_PROGRAM")
.and_then(|v| v.as_str().ok())
== Some("vscode")
{
// We're in vscode and we have osc633 enabled
Some(format!( Some(format!(
"{VSCODE_PRE_PROMPT_MARKER}{configured_left_prompt_string}{VSCODE_POST_PROMPT_MARKER}" "{PRE_PROMPT_MARKER}{prompt_string}{POST_PROMPT_MARKER}"
))
} else if config.shell_integration.osc133 {
// If we're in VSCode but we don't find the env var, but we have osc133 set, then use it
Some(format!(
"{PRE_PROMPT_MARKER}{configured_left_prompt_string}{POST_PROMPT_MARKER}"
)) ))
} else { } else {
configured_left_prompt_string.into() left_prompt_string
} }
} else if config.shell_integration.osc133 {
Some(format!(
"{PRE_PROMPT_MARKER}{configured_left_prompt_string}{POST_PROMPT_MARKER}"
))
} else { } else {
configured_left_prompt_string.into() left_prompt_string
}; };
let right_prompt_string = get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, stack); let right_prompt_string = get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, stack);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,9 @@ use nu_color_config::{get_matching_brackets_style, get_shape_color};
use nu_engine::env; use nu_engine::env;
use nu_parser::{flatten_block, parse, FlatShape}; use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::{ use nu_protocol::{
ast::{Block, Expr, Expression, PipelineRedirection, RecordItem}, ast::{Argument, Block, Expr, Expression, PipelineRedirection, RecordItem},
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, Config, Span,
}; };
use reedline::{Highlighter, StyledText}; use reedline::{Highlighter, StyledText};
use std::sync::Arc; use std::sync::Arc;
@ -14,14 +14,15 @@ use std::sync::Arc;
pub struct NuHighlighter { pub struct NuHighlighter {
pub engine_state: Arc<EngineState>, pub engine_state: Arc<EngineState>,
pub stack: Arc<Stack>, pub stack: Arc<Stack>,
pub config: Config,
} }
impl Highlighter for NuHighlighter { impl Highlighter for NuHighlighter {
fn highlight(&self, line: &str, _cursor: usize) -> StyledText { fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
trace!("highlighting: {}", line); trace!("highlighting: {}", line);
let config = self.stack.get_config(&self.engine_state); let highlight_resolved_externals =
let highlight_resolved_externals = config.highlight_resolved_externals; self.engine_state.get_config().highlight_resolved_externals;
let mut working_set = StateWorkingSet::new(&self.engine_state); let mut working_set = StateWorkingSet::new(&self.engine_state);
let block = parse(&mut working_set, None, line.as_bytes(), false); let block = parse(&mut working_set, None, line.as_bytes(), false);
let (shapes, global_span_offset) = { let (shapes, global_span_offset) = {
@ -36,7 +37,6 @@ impl Highlighter for NuHighlighter {
let str_word = String::from_utf8_lossy(str_contents).to_string(); let str_word = String::from_utf8_lossy(str_contents).to_string();
let paths = env::path_str(&self.engine_state, &self.stack, *span).ok(); let paths = env::path_str(&self.engine_state, &self.stack, *span).ok();
#[allow(deprecated)]
let res = if let Ok(cwd) = let res = if let Ok(cwd) =
env::current_dir_str(&self.engine_state, &self.stack) env::current_dir_str(&self.engine_state, &self.stack)
{ {
@ -86,8 +86,29 @@ impl Highlighter for NuHighlighter {
[(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)] [(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)]
.to_string(); .to_string();
macro_rules! add_colored_token_with_bracket_highlight {
($shape:expr, $span:expr, $text:expr) => {{
let spans = split_span_by_highlight_positions(
line,
$span,
&matching_brackets_pos,
global_span_offset,
);
spans.iter().for_each(|(part, highlight)| {
let start = part.start - $span.start;
let end = part.end - $span.start;
let text = (&next_token[start..end]).to_string();
let mut style = get_shape_color($shape.to_string(), &self.config);
if *highlight {
style = get_matching_brackets_style(style, &self.config);
}
output.push((style, text));
});
}};
}
let mut add_colored_token = |shape: &FlatShape, text: String| { let mut add_colored_token = |shape: &FlatShape, text: String| {
output.push((get_shape_color(shape.as_str(), &config), text)); output.push((get_shape_color(shape.to_string(), &self.config), text));
}; };
match shape.1 { match shape.1 {
@ -107,43 +128,35 @@ impl Highlighter for NuHighlighter {
FlatShape::Operator => add_colored_token(&shape.1, next_token), FlatShape::Operator => add_colored_token(&shape.1, next_token),
FlatShape::Signature => add_colored_token(&shape.1, next_token), FlatShape::Signature => add_colored_token(&shape.1, next_token),
FlatShape::String => add_colored_token(&shape.1, next_token), FlatShape::String => add_colored_token(&shape.1, next_token),
FlatShape::RawString => add_colored_token(&shape.1, next_token),
FlatShape::StringInterpolation => add_colored_token(&shape.1, next_token), FlatShape::StringInterpolation => add_colored_token(&shape.1, next_token),
FlatShape::DateTime => add_colored_token(&shape.1, next_token), FlatShape::DateTime => add_colored_token(&shape.1, next_token),
FlatShape::List FlatShape::List => {
| FlatShape::Table add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
| FlatShape::Record }
| FlatShape::Block FlatShape::Table => {
| FlatShape::Closure => { add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
let span = shape.0; }
let shape = &shape.1; FlatShape::Record => {
let spans = split_span_by_highlight_positions( add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
line, }
span,
&matching_brackets_pos, FlatShape::Block => {
global_span_offset, add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
); }
for (part, highlight) in spans { FlatShape::Closure => {
let start = part.start - span.start; add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
let end = part.end - span.start;
let text = next_token[start..end].to_string();
let mut style = get_shape_color(shape.as_str(), &config);
if highlight {
style = get_matching_brackets_style(style, &config);
}
output.push((style, text));
}
} }
FlatShape::Filepath => add_colored_token(&shape.1, next_token), FlatShape::Filepath => add_colored_token(&shape.1, next_token),
FlatShape::Directory => add_colored_token(&shape.1, next_token), FlatShape::Directory => add_colored_token(&shape.1, next_token),
FlatShape::GlobInterpolation => add_colored_token(&shape.1, next_token),
FlatShape::GlobPattern => add_colored_token(&shape.1, next_token), FlatShape::GlobPattern => add_colored_token(&shape.1, next_token),
FlatShape::Variable(_) | FlatShape::VarDecl(_) => { FlatShape::Variable(_) | FlatShape::VarDecl(_) => {
add_colored_token(&shape.1, next_token) add_colored_token(&shape.1, next_token)
} }
FlatShape::Flag => add_colored_token(&shape.1, next_token), FlatShape::Flag => add_colored_token(&shape.1, next_token),
FlatShape::Pipe => add_colored_token(&shape.1, next_token), FlatShape::Pipe => add_colored_token(&shape.1, next_token),
FlatShape::And => add_colored_token(&shape.1, next_token),
FlatShape::Or => add_colored_token(&shape.1, next_token),
FlatShape::Redirection => add_colored_token(&shape.1, next_token), FlatShape::Redirection => add_colored_token(&shape.1, next_token),
FlatShape::Custom(..) => add_colored_token(&shape.1, next_token), FlatShape::Custom(..) => add_colored_token(&shape.1, next_token),
FlatShape::MatchPattern => add_colored_token(&shape.1, next_token), FlatShape::MatchPattern => add_colored_token(&shape.1, next_token),
@ -297,6 +310,20 @@ fn find_matching_block_end_in_expr(
global_span_offset: usize, global_span_offset: usize,
global_cursor_offset: usize, global_cursor_offset: usize,
) -> Option<usize> { ) -> Option<usize> {
macro_rules! find_in_expr_or_continue {
($inner_expr:ident) => {
if let Some(pos) = find_matching_block_end_in_expr(
line,
working_set,
$inner_expr,
global_span_offset,
global_cursor_offset,
) {
return Some(pos);
}
};
}
if expression.span.contains(global_cursor_offset) && expression.span.start >= global_span_offset if expression.span.contains(global_cursor_offset) && expression.span.start >= global_span_offset
{ {
let expr_first = expression.span.start; let expr_first = expression.span.start;
@ -309,7 +336,6 @@ fn find_matching_block_end_in_expr(
.unwrap_or(expression.span.start); .unwrap_or(expression.span.start);
return match &expression.expr { return match &expression.expr {
// TODO: Can't these be handled with an `_ => None` branch? Refactor
Expr::Bool(_) => None, Expr::Bool(_) => None,
Expr::Int(_) => None, Expr::Int(_) => None,
Expr::Float(_) => None, Expr::Float(_) => None,
@ -327,7 +353,6 @@ fn find_matching_block_end_in_expr(
Expr::Directory(_, _) => None, Expr::Directory(_, _) => None,
Expr::GlobPattern(_, _) => None, Expr::GlobPattern(_, _) => None,
Expr::String(_) => None, Expr::String(_) => None,
Expr::RawString(_) => None,
Expr::CellPath(_) => None, Expr::CellPath(_) => None,
Expr::ImportPattern(_) => None, Expr::ImportPattern(_) => None,
Expr::Overlay(_) => None, Expr::Overlay(_) => None,
@ -335,30 +360,9 @@ fn find_matching_block_end_in_expr(
Expr::MatchBlock(_) => None, Expr::MatchBlock(_) => None,
Expr::Nothing => None, Expr::Nothing => None,
Expr::Garbage => None, Expr::Garbage => None,
Expr::Spread(_) => None,
Expr::AttributeBlock(ab) => ab Expr::Table(hdr, rows) => {
.attributes
.iter()
.find_map(|attr| {
find_matching_block_end_in_expr(
line,
working_set,
&attr.expr,
global_span_offset,
global_cursor_offset,
)
})
.or_else(|| {
find_matching_block_end_in_expr(
line,
working_set,
&ab.item,
global_span_offset,
global_cursor_offset,
)
}),
Expr::Table(table) => {
if expr_last == global_cursor_offset { if expr_last == global_cursor_offset {
// cursor is at table end // cursor is at table end
Some(expr_first) Some(expr_first)
@ -367,19 +371,15 @@ fn find_matching_block_end_in_expr(
Some(expr_last) Some(expr_last)
} else { } else {
// cursor is inside table // cursor is inside table
table for inner_expr in hdr {
.columns find_in_expr_or_continue!(inner_expr);
.iter() }
.chain(table.rows.iter().flat_map(AsRef::as_ref)) for row in rows {
.find_map(|expr| { for inner_expr in row {
find_matching_block_end_in_expr( find_in_expr_or_continue!(inner_expr);
line, }
working_set, }
expr, None
global_span_offset,
global_cursor_offset,
)
})
} }
} }
@ -392,45 +392,36 @@ fn find_matching_block_end_in_expr(
Some(expr_last) Some(expr_last)
} else { } else {
// cursor is inside record // cursor is inside record
exprs.iter().find_map(|expr| match expr { for expr in exprs {
RecordItem::Pair(k, v) => find_matching_block_end_in_expr( match expr {
line, RecordItem::Pair(k, v) => {
working_set, find_in_expr_or_continue!(k);
k, find_in_expr_or_continue!(v);
global_span_offset, }
global_cursor_offset, RecordItem::Spread(_, record) => {
) find_in_expr_or_continue!(record);
.or_else(|| { }
find_matching_block_end_in_expr( }
line, }
working_set, None
v,
global_span_offset,
global_cursor_offset,
)
}),
RecordItem::Spread(_, record) => find_matching_block_end_in_expr(
line,
working_set,
record,
global_span_offset,
global_cursor_offset,
),
})
} }
} }
Expr::Call(call) => call.arguments.iter().find_map(|arg| { Expr::Call(call) => {
arg.expr().and_then(|expr| { for arg in &call.arguments {
find_matching_block_end_in_expr( let opt_expr = match arg {
line, Argument::Named((_, _, opt_expr)) => opt_expr.as_ref(),
working_set, Argument::Positional(inner_expr) => Some(inner_expr),
expr, Argument::Unknown(inner_expr) => Some(inner_expr),
global_span_offset, Argument::Spread(inner_expr) => Some(inner_expr),
global_cursor_offset, };
)
}) if let Some(inner_expr) = opt_expr {
}), find_in_expr_or_continue!(inner_expr);
}
}
None
}
Expr::FullCellPath(b) => find_matching_block_end_in_expr( Expr::FullCellPath(b) => find_matching_block_end_in_expr(
line, line,
@ -440,23 +431,12 @@ fn find_matching_block_end_in_expr(
global_cursor_offset, global_cursor_offset,
), ),
Expr::BinaryOp(lhs, op, rhs) => [lhs, op, rhs].into_iter().find_map(|expr| { Expr::BinaryOp(lhs, op, rhs) => {
find_matching_block_end_in_expr( find_in_expr_or_continue!(lhs);
line, find_in_expr_or_continue!(op);
working_set, find_in_expr_or_continue!(rhs);
expr, None
global_span_offset, }
global_cursor_offset,
)
}),
Expr::Collect(_, expr) => find_matching_block_end_in_expr(
line,
working_set,
expr,
global_span_offset,
global_cursor_offset,
),
Expr::Block(block_id) Expr::Block(block_id)
| Expr::Closure(block_id) | Expr::Closure(block_id)
@ -481,19 +461,14 @@ fn find_matching_block_end_in_expr(
} }
} }
Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => { Expr::StringInterpolation(inner_expr) => {
exprs.iter().find_map(|expr| { for inner_expr in inner_expr {
find_matching_block_end_in_expr( find_in_expr_or_continue!(inner_expr);
line, }
working_set, None
expr,
global_span_offset,
global_cursor_offset,
)
})
} }
Expr::List(list) => { Expr::List(inner_expr) => {
if expr_last == global_cursor_offset { if expr_last == global_cursor_offset {
// cursor is at list end // cursor is at list end
Some(expr_first) Some(expr_first)
@ -501,15 +476,11 @@ fn find_matching_block_end_in_expr(
// cursor is at list start // cursor is at list start
Some(expr_last) Some(expr_last)
} else { } else {
list.iter().find_map(|item| { // cursor is inside list
find_matching_block_end_in_expr( for inner_expr in inner_expr {
line, find_in_expr_or_continue!(inner_expr);
working_set, }
item.expr(), None
global_span_offset,
global_cursor_offset,
)
})
} }
} }
}; };

View File

@ -1,18 +1,14 @@
#![allow(clippy::byte_char_slices)]
use nu_cmd_base::hook::eval_hook; use nu_cmd_base::hook::eval_hook;
use nu_engine::{eval_block, eval_block_with_early_return}; use nu_engine::{eval_block, eval_block_with_early_return};
use nu_parser::{lex, parse, unescape_unquote_string, Token, TokenContents}; use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents};
use nu_protocol::{ use nu_protocol::{
cli_error::report_compile_error,
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
report_parse_error, report_parse_warning, report_shell_error, PipelineData, ShellError, Span, print_if_stream, report_error, report_error_new, PipelineData, ShellError, Span, Value,
Value,
}; };
#[cfg(windows)] #[cfg(windows)]
use nu_utils::enable_vt_processing; use nu_utils::enable_vt_processing;
use nu_utils::{escape_quote_string, perf}; use nu_utils::utils::perf;
use std::path::Path; use std::path::Path;
// This will collect environment variables from std::env and adds them to a stack. // This will collect environment variables from std::env and adds them to a stack.
@ -43,8 +39,9 @@ fn gather_env_vars(
init_cwd: &Path, init_cwd: &Path,
) { ) {
fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) { fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
report_shell_error( let working_set = StateWorkingSet::new(engine_state);
engine_state, report_error(
&working_set,
&ShellError::GenericError { &ShellError::GenericError {
error: format!("Environment variable was not captured: {env_str}"), error: format!("Environment variable was not captured: {env_str}"),
msg: "".into(), msg: "".into(),
@ -74,8 +71,9 @@ fn gather_env_vars(
} }
None => { None => {
// Could not capture current working directory // Could not capture current working directory
report_shell_error( let working_set = StateWorkingSet::new(engine_state);
engine_state, report_error(
&working_set,
&ShellError::GenericError { &ShellError::GenericError {
error: "Current directory is not a valid utf-8 path".into(), error: "Current directory is not a valid utf-8 path".into(),
msg: "".into(), msg: "".into(),
@ -132,7 +130,7 @@ fn gather_env_vars(
working_set.error(err); working_set.error(err);
} }
if !working_set.parse_errors.is_empty() { if working_set.parse_errors.first().is_some() {
report_capture_error( report_capture_error(
engine_state, engine_state,
&String::from_utf8_lossy(contents), &String::from_utf8_lossy(contents),
@ -176,7 +174,7 @@ fn gather_env_vars(
working_set.error(err); working_set.error(err);
} }
if !working_set.parse_errors.is_empty() { if working_set.parse_errors.first().is_some() {
report_capture_error( report_capture_error(
engine_state, engine_state,
&String::from_utf8_lossy(contents), &String::from_utf8_lossy(contents),
@ -203,35 +201,6 @@ fn gather_env_vars(
} }
} }
/// Print a pipeline with formatting applied based on display_output hook.
///
/// This function should be preferred when printing values resulting from a completed evaluation.
/// For values printed as part of a command's execution, such as values printed by the `print` command,
/// the `PipelineData::print_table` function should be preferred instead as it is not config-dependent.
///
/// `no_newline` controls if we need to attach newline character to output.
pub fn print_pipeline(
engine_state: &mut EngineState,
stack: &mut Stack,
pipeline: PipelineData,
no_newline: bool,
) -> Result<(), ShellError> {
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
let pipeline = eval_hook(
engine_state,
stack,
Some(pipeline),
vec![],
&hook,
"display_output",
)?;
pipeline.print_raw(engine_state, no_newline, false)
} else {
// if display_output isn't set, we should still prefer to print with some formatting
pipeline.print_table(engine_state, stack, no_newline, false)
}
}
pub fn eval_source( pub fn eval_source(
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
@ -239,49 +208,9 @@ pub fn eval_source(
fname: &str, fname: &str,
input: PipelineData, input: PipelineData,
allow_return: bool, allow_return: bool,
) -> i32 { ) -> bool {
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
let exit_code = match evaluate_source(engine_state, stack, source, fname, input, allow_return) {
Ok(failed) => {
let code = failed.into();
stack.set_last_exit_code(code, Span::unknown());
code
}
Err(err) => {
report_shell_error(engine_state, &err);
let code = err.exit_code();
stack.set_last_error(&err);
code.unwrap_or(0)
}
};
// reset vt processing, aka ansi because illbehaved externals can break it
#[cfg(windows)]
{
let _ = enable_vt_processing();
}
perf!(
&format!("eval_source {}", &fname),
start_time,
engine_state
.get_config()
.use_ansi_coloring
.get(engine_state)
);
exit_code
}
fn evaluate_source(
engine_state: &mut EngineState,
stack: &mut Stack,
source: &[u8],
fname: &str,
input: PipelineData,
allow_return: bool,
) -> Result<bool, ShellError> {
let (block, delta) = { let (block, delta) = {
let mut working_set = StateWorkingSet::new(engine_state); let mut working_set = StateWorkingSet::new(engine_state);
let output = parse( let output = parse(
@ -291,34 +220,108 @@ fn evaluate_source(
false, false,
); );
if let Some(warning) = working_set.parse_warnings.first() { if let Some(warning) = working_set.parse_warnings.first() {
report_parse_warning(&working_set, warning); report_error(&working_set, warning);
} }
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
report_parse_error(&working_set, err); set_last_exit_code(stack, 1);
return Ok(true); report_error(&working_set, err);
} return false;
if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err);
return Ok(true);
} }
(output, working_set.render()) (output, working_set.render())
}; };
engine_state.merge_delta(delta)?; if let Err(err) = engine_state.merge_delta(delta) {
set_last_exit_code(stack, 1);
report_error_new(engine_state, &err);
return false;
}
let pipeline = if allow_return { let b = if allow_return {
eval_block_with_early_return::<WithoutDebug>(engine_state, stack, &block, input) eval_block_with_early_return::<WithoutDebug>(engine_state, stack, &block, input)
} else { } else {
eval_block::<WithoutDebug>(engine_state, stack, &block, input) eval_block::<WithoutDebug>(engine_state, stack, &block, input)
}?; };
let no_newline = matches!(&pipeline, &PipelineData::ByteStream(..)); match b {
print_pipeline(engine_state, stack, pipeline, no_newline)?; Ok(pipeline_data) => {
let config = engine_state.get_config();
let result;
if let PipelineData::ExternalStream {
stdout: stream,
stderr: stderr_stream,
exit_code,
..
} = pipeline_data
{
result = print_if_stream(stream, stderr_stream, false, exit_code);
} else if let Some(hook) = config.hooks.display_output.clone() {
match eval_hook(
engine_state,
stack,
Some(pipeline_data),
vec![],
&hook,
"display_output",
) {
Err(err) => {
result = Err(err);
}
Ok(val) => {
result = val.print(engine_state, stack, false, false);
}
}
} else {
result = pipeline_data.print(engine_state, stack, true, false);
}
Ok(false) match result {
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
return false;
}
Ok(exit_code) => {
set_last_exit_code(stack, exit_code);
}
}
// reset vt processing, aka ansi because illbehaved externals can break it
#[cfg(windows)]
{
let _ = enable_vt_processing();
}
}
Err(err) => {
set_last_exit_code(stack, 1);
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
return false;
}
}
perf(
&format!("eval_source {}", &fname),
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
);
true
}
fn set_last_exit_code(stack: &mut Stack, exit_code: i64) {
stack.add_env_var(
"LAST_EXIT_CODE".to_string(),
Value::int(exit_code, Span::unknown()),
);
} }
#[cfg(test)] #[cfg(test)]
@ -343,10 +346,16 @@ mod test {
let env = engine_state.render_env_vars(); let env = engine_state.render_env_vars();
assert!(matches!(env.get("FOO"), Some(&Value::String { val, .. }) if val == "foo")); assert!(
assert!(matches!(env.get("SYMBOLS"), Some(&Value::String { val, .. }) if val == symbols)); matches!(env.get(&"FOO".to_string()), Some(&Value::String { val, .. }) if val == "foo")
assert!(matches!(env.get(symbols), Some(&Value::String { val, .. }) if val == "symbols")); );
assert!(env.contains_key("PWD")); assert!(
matches!(env.get(&"SYMBOLS".to_string()), Some(&Value::String { val, .. }) if val == symbols)
);
assert!(
matches!(env.get(&symbols.to_string()), Some(&Value::String { val, .. }) if val == "symbols")
);
assert!(env.get(&"PWD".to_string()).is_some());
assert_eq!(env.len(), 4); assert_eq!(env.len(), 4);
} }
} }

View File

@ -1,296 +0,0 @@
use nu_protocol::HistoryFileFormat;
use nu_test_support::{nu, Outcome};
use reedline::{
FileBackedHistory, History, HistoryItem, HistoryItemId, ReedlineError, SearchQuery,
SqliteBackedHistory,
};
use rstest::rstest;
use tempfile::TempDir;
struct Test {
cfg_dir: TempDir,
}
impl Test {
fn new(history_format: &'static str) -> Self {
let cfg_dir = tempfile::Builder::new()
.prefix("history_import_test")
.tempdir()
.unwrap();
// Assigning to $env.config.history.file_format seems to work only in startup
// configuration.
std::fs::write(
cfg_dir.path().join("env.nu"),
format!("$env.config.history.file_format = {history_format:?}"),
)
.unwrap();
Self { cfg_dir }
}
fn nu(&self, cmd: impl AsRef<str>) -> Outcome {
let env = [(
"XDG_CONFIG_HOME".to_string(),
self.cfg_dir.path().to_str().unwrap().to_string(),
)];
let env_config = self.cfg_dir.path().join("env.nu");
nu!(envs: env, env_config: env_config, cmd.as_ref())
}
fn open_plaintext(&self) -> Result<FileBackedHistory, ReedlineError> {
FileBackedHistory::with_file(
100,
self.cfg_dir
.path()
.join("nushell")
.join(HistoryFileFormat::Plaintext.default_file_name()),
)
}
fn open_sqlite(&self) -> Result<SqliteBackedHistory, ReedlineError> {
SqliteBackedHistory::with_file(
self.cfg_dir
.path()
.join("nushell")
.join(HistoryFileFormat::Sqlite.default_file_name()),
None,
None,
)
}
fn open_backend(&self, format: HistoryFileFormat) -> Result<Box<dyn History>, ReedlineError> {
fn boxed(be: impl History + 'static) -> Box<dyn History> {
Box::new(be)
}
use HistoryFileFormat::*;
match format {
Plaintext => self.open_plaintext().map(boxed),
Sqlite => self.open_sqlite().map(boxed),
}
}
}
enum HistorySource {
Vec(Vec<HistoryItem>),
Command(&'static str),
}
struct TestCase {
dst_format: HistoryFileFormat,
dst_history: Vec<HistoryItem>,
src_history: HistorySource,
want_history: Vec<HistoryItem>,
}
const EMPTY_TEST_CASE: TestCase = TestCase {
dst_format: HistoryFileFormat::Plaintext,
dst_history: Vec::new(),
src_history: HistorySource::Vec(Vec::new()),
want_history: Vec::new(),
};
impl TestCase {
fn run(self) {
use HistoryFileFormat::*;
let test = Test::new(match self.dst_format {
Plaintext => "plaintext",
Sqlite => "sqlite",
});
save_all(
&mut *test.open_backend(self.dst_format).unwrap(),
self.dst_history,
)
.unwrap();
let outcome = match self.src_history {
HistorySource::Vec(src_history) => {
let src_format = match self.dst_format {
Plaintext => Sqlite,
Sqlite => Plaintext,
};
save_all(&mut *test.open_backend(src_format).unwrap(), src_history).unwrap();
test.nu("history import")
}
HistorySource::Command(cmd) => {
let mut cmd = cmd.to_string();
cmd.push_str(" | history import");
test.nu(cmd)
}
};
assert!(outcome.status.success());
let got = query_all(&*test.open_backend(self.dst_format).unwrap()).unwrap();
// Compare just the commands first, for readability.
fn commands_only(items: &[HistoryItem]) -> Vec<&str> {
items
.iter()
.map(|item| item.command_line.as_str())
.collect()
}
assert_eq!(commands_only(&got), commands_only(&self.want_history));
// If commands match, compare full items.
assert_eq!(got, self.want_history);
}
}
fn query_all(history: &dyn History) -> Result<Vec<HistoryItem>, ReedlineError> {
history.search(SearchQuery::everything(
reedline::SearchDirection::Forward,
None,
))
}
fn save_all(history: &mut dyn History, items: Vec<HistoryItem>) -> Result<(), ReedlineError> {
for item in items {
history.save(item)?;
}
Ok(())
}
const EMPTY_ITEM: HistoryItem = HistoryItem {
command_line: String::new(),
id: None,
start_timestamp: None,
session_id: None,
hostname: None,
cwd: None,
duration: None,
exit_status: None,
more_info: None,
};
#[test]
fn history_import_pipe_string() {
TestCase {
dst_format: HistoryFileFormat::Plaintext,
src_history: HistorySource::Command("echo bar"),
want_history: vec![HistoryItem {
id: Some(HistoryItemId::new(0)),
command_line: "bar".to_string(),
..EMPTY_ITEM
}],
..EMPTY_TEST_CASE
}
.run();
}
#[test]
fn history_import_pipe_record() {
TestCase {
dst_format: HistoryFileFormat::Sqlite,
src_history: HistorySource::Command("[[cwd command]; [/tmp some_command]]"),
want_history: vec![HistoryItem {
id: Some(HistoryItemId::new(1)),
command_line: "some_command".to_string(),
cwd: Some("/tmp".to_string()),
..EMPTY_ITEM
}],
..EMPTY_TEST_CASE
}
.run();
}
#[test]
fn to_empty_plaintext() {
TestCase {
dst_format: HistoryFileFormat::Plaintext,
src_history: HistorySource::Vec(vec![
HistoryItem {
command_line: "foo".to_string(),
..EMPTY_ITEM
},
HistoryItem {
command_line: "bar".to_string(),
..EMPTY_ITEM
},
]),
want_history: vec![
HistoryItem {
id: Some(HistoryItemId::new(0)),
command_line: "foo".to_string(),
..EMPTY_ITEM
},
HistoryItem {
id: Some(HistoryItemId::new(1)),
command_line: "bar".to_string(),
..EMPTY_ITEM
},
],
..EMPTY_TEST_CASE
}
.run()
}
#[test]
fn to_empty_sqlite() {
TestCase {
dst_format: HistoryFileFormat::Sqlite,
src_history: HistorySource::Vec(vec![
HistoryItem {
command_line: "foo".to_string(),
..EMPTY_ITEM
},
HistoryItem {
command_line: "bar".to_string(),
..EMPTY_ITEM
},
]),
want_history: vec![
HistoryItem {
id: Some(HistoryItemId::new(1)),
command_line: "foo".to_string(),
..EMPTY_ITEM
},
HistoryItem {
id: Some(HistoryItemId::new(2)),
command_line: "bar".to_string(),
..EMPTY_ITEM
},
],
..EMPTY_TEST_CASE
}
.run()
}
#[rstest]
#[case::plaintext(HistoryFileFormat::Plaintext)]
#[case::sqlite(HistoryFileFormat::Sqlite)]
fn to_existing(#[case] dst_format: HistoryFileFormat) {
TestCase {
dst_format,
dst_history: vec![
HistoryItem {
id: Some(HistoryItemId::new(0)),
command_line: "original-1".to_string(),
..EMPTY_ITEM
},
HistoryItem {
id: Some(HistoryItemId::new(1)),
command_line: "original-2".to_string(),
..EMPTY_ITEM
},
],
src_history: HistorySource::Vec(vec![HistoryItem {
id: Some(HistoryItemId::new(1)),
command_line: "new".to_string(),
..EMPTY_ITEM
}]),
want_history: vec![
HistoryItem {
id: Some(HistoryItemId::new(0)),
command_line: "original-1".to_string(),
..EMPTY_ITEM
},
HistoryItem {
id: Some(HistoryItemId::new(1)),
command_line: "original-2".to_string(),
..EMPTY_ITEM
},
HistoryItem {
id: Some(HistoryItemId::new(2)),
command_line: "new".to_string(),
..EMPTY_ITEM
},
],
}
.run()
}

View File

@ -1,7 +0,0 @@
use nu_test_support::nu;
#[test]
fn not_empty() {
let result = nu!("keybindings list | is-not-empty");
assert_eq!(result.out, "true");
}

View File

@ -1,3 +0,0 @@
mod history_import;
mod keybindings_list;
mod nu_highlight;

View File

@ -1,7 +0,0 @@
use nu_test_support::nu;
#[test]
fn nu_highlight_not_expr() {
let actual = nu!("'not false' | nu-highlight | ansi strip");
assert_eq!(actual.out, "not false");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
pub mod completions_helpers;
pub use completions_helpers::{
file, folder, match_suggestions, match_suggestions_by_string, merge_input, new_engine,
};

View File

@ -1,2 +0,0 @@
mod commands;
mod completions;

View File

@ -1,34 +1,39 @@
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_parser::parse; use nu_parser::parse;
use nu_path::{AbsolutePathBuf, PathBuf};
use nu_protocol::{ use nu_protocol::{
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
PipelineData, ShellError, Span, Value, eval_const::create_nu_constant,
PipelineData, ShellError, Span, Value, NU_VARIABLE_ID,
}; };
use nu_test_support::fs; use nu_test_support::fs;
use reedline::Suggestion; use reedline::Suggestion;
use std::path::MAIN_SEPARATOR; use std::path::PathBuf;
const SEP: char = std::path::MAIN_SEPARATOR;
fn create_default_context() -> EngineState { fn create_default_context() -> EngineState {
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()) nu_command::add_shell_command_context(nu_cmd_lang::create_default_context())
} }
/// creates a new engine with the current path into the completions fixtures folder // creates a new engine with the current path into the completions fixtures folder
pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { pub fn new_engine() -> (PathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("completions"); let dir = fs::fixtures().join("completions");
let dir_str = dir let mut dir_str = dir
.clone() .clone()
.into_os_string() .into_os_string()
.into_string() .into_string()
.unwrap_or_default(); .unwrap_or_default();
dir_str.push(SEP);
// Create a new engine with default context // Create a new engine with default context
let mut engine_state = create_default_context(); let mut engine_state = create_default_context();
// Add $nu // Add $nu
engine_state.generate_nu_constant(); let nu_const =
create_nu_constant(&engine_state, Span::test_data()).expect("Failed creating $nu");
engine_state.set_variable_const_val(NU_VARIABLE_ID, nu_const);
// New stack // New stack
let mut stack = Stack::new(); let mut stack = Stack::new();
@ -63,97 +68,21 @@ pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
); );
// Merge environment into the permanent state // Merge environment into the permanent state
let merge_result = engine_state.merge_env(&mut stack); let merge_result = engine_state.merge_env(&mut stack, &dir);
assert!(merge_result.is_ok()); assert!(merge_result.is_ok());
(dir, dir_str, engine_state, stack) (dir, dir_str, engine_state, stack)
} }
/// Adds pseudo PATH env for external completion tests pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) {
pub fn new_external_engine() -> EngineState {
let mut engine = create_default_context();
let dir = fs::fixtures().join("external_completions").join("path");
let dir_str = dir.to_string_lossy().to_string();
let internal_span = nu_protocol::Span::new(0, dir_str.len());
engine.add_env_var(
"PATH".to_string(),
Value::List {
vals: vec![Value::String {
val: dir_str,
internal_span,
}],
internal_span,
},
);
engine
}
/// creates a new engine with the current path into the completions fixtures folder
pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
// Target folder inside assets
let dir = fs::fixtures().join("dotnu_completions");
let dir_str = dir
.clone()
.into_os_string()
.into_string()
.unwrap_or_default();
let dir_span = nu_protocol::Span::new(0, dir_str.len());
// Create a new engine with default context
let mut engine_state = create_default_context();
// Add $nu
engine_state.generate_nu_constant();
// const $NU_LIB_DIRS
let mut working_set = StateWorkingSet::new(&engine_state);
let var_id = working_set.add_variable(
b"$NU_LIB_DIRS".into(),
Span::unknown(),
nu_protocol::Type::List(Box::new(nu_protocol::Type::String)),
false,
);
working_set.set_variable_const_val(
var_id,
Value::test_list(vec![
Value::string(file(dir.join("lib-dir1")), dir_span),
Value::string(file(dir.join("lib-dir3")), dir_span),
]),
);
let _ = engine_state.merge_delta(working_set.render());
// New stack
let mut stack = Stack::new();
// Add pwd as env var
stack.add_env_var("PWD".to_string(), Value::string(dir_str.clone(), dir_span));
stack.add_env_var(
"TEST".to_string(),
Value::string("NUSHELL".to_string(), dir_span),
);
stack.add_env_var(
"NU_LIB_DIRS".into(),
Value::test_list(vec![
Value::string(file(dir.join("lib-dir2")), dir_span),
Value::string(file(dir.join("lib-dir3")), dir_span),
]),
);
// Merge environment into the permanent state
let merge_result = engine_state.merge_env(&mut stack);
assert!(merge_result.is_ok());
(dir, dir_str, engine_state, stack)
}
pub fn new_quote_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("quoted_completions"); let dir = fs::fixtures().join("quoted_completions");
let dir_str = dir let mut dir_str = dir
.clone() .clone()
.into_os_string() .into_os_string()
.into_string() .into_string()
.unwrap_or_default(); .unwrap_or_default();
dir_str.push(SEP);
// Create a new engine with default context // Create a new engine with default context
let mut engine_state = create_default_context(); let mut engine_state = create_default_context();
@ -175,20 +104,21 @@ pub fn new_quote_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
); );
// Merge environment into the permanent state // Merge environment into the permanent state
let merge_result = engine_state.merge_env(&mut stack); let merge_result = engine_state.merge_env(&mut stack, &dir);
assert!(merge_result.is_ok()); assert!(merge_result.is_ok());
(dir, dir_str, engine_state, stack) (dir, dir_str, engine_state, stack)
} }
pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { pub fn new_partial_engine() -> (PathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("partial_completions"); let dir = fs::fixtures().join("partial_completions");
let dir_str = dir let mut dir_str = dir
.clone() .clone()
.into_os_string() .into_os_string()
.into_string() .into_string()
.unwrap_or_default(); .unwrap_or_default();
dir_str.push(SEP);
// Create a new engine with default context // Create a new engine with default context
let mut engine_state = create_default_context(); let mut engine_state = create_default_context();
@ -210,14 +140,14 @@ pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
); );
// Merge environment into the permanent state // Merge environment into the permanent state
let merge_result = engine_state.merge_env(&mut stack); let merge_result = engine_state.merge_env(&mut stack, &dir);
assert!(merge_result.is_ok()); assert!(merge_result.is_ok());
(dir, dir_str, engine_state, stack) (dir, dir_str, engine_state, stack)
} }
/// match a list of suggestions with the expected values // match a list of suggestions with the expected values
pub fn match_suggestions(expected: &Vec<&str>, suggestions: &Vec<Suggestion>) { pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
let expected_len = expected.len(); let expected_len = expected.len();
let suggestions_len = suggestions.len(); let suggestions_len = suggestions.len();
if expected_len != suggestions_len { if expected_len != suggestions_len {
@ -227,39 +157,31 @@ pub fn match_suggestions(expected: &Vec<&str>, suggestions: &Vec<Suggestion>) {
Expected: {expected:#?}\n" Expected: {expected:#?}\n"
) )
} }
expected.iter().zip(suggestions).for_each(|it| {
let suggestions_str = suggestions assert_eq!(it.0, &it.1.value);
.iter() });
.map(|it| it.value.as_str())
.collect::<Vec<_>>();
assert_eq!(expected, &suggestions_str);
} }
/// match a list of suggestions with the expected values // append the separator to the converted path
pub fn match_suggestions_by_string(expected: &[String], suggestions: &Vec<Suggestion>) { pub fn folder(path: PathBuf) -> String {
let expected = expected.iter().map(|it| it.as_str()).collect::<Vec<_>>();
match_suggestions(&expected, suggestions);
}
/// append the separator to the converted path
pub fn folder(path: impl Into<PathBuf>) -> String {
let mut converted_path = file(path); let mut converted_path = file(path);
converted_path.push(MAIN_SEPARATOR); converted_path.push(SEP);
converted_path converted_path
} }
/// convert a given path to string // convert a given path to string
pub fn file(path: impl Into<PathBuf>) -> String { pub fn file(path: PathBuf) -> String {
path.into().into_os_string().into_string().unwrap() path.into_os_string().into_string().unwrap_or_default()
} }
/// merge_input executes the given input into the engine // merge_input executes the given input into the engine
/// and merges the state // and merges the state
pub fn merge_input( pub fn merge_input(
input: &[u8], input: &[u8],
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
dir: PathBuf,
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
let (block, delta) = { let (block, delta) = {
let mut working_set = StateWorkingSet::new(engine_state); let mut working_set = StateWorkingSet::new(engine_state);
@ -282,5 +204,5 @@ pub fn merge_input(
.is_ok()); .is_ok());
// Merge environment into the permanent state // Merge environment into the permanent state
engine_state.merge_env(stack) engine_state.merge_env(stack, &dir)
} }

View File

@ -0,0 +1,3 @@
pub mod completions_helpers;
pub use completions_helpers::{file, folder, match_suggestions, merge_input, new_engine};

View File

@ -5,20 +5,17 @@ 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.104.1" version = "0.92.2"
# 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
[lints]
workspace = true
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.104.1", default-features = false } nu-engine = { path = "../nu-engine", version = "0.92.2" }
nu-parser = { path = "../nu-parser", version = "0.104.1" } nu-parser = { path = "../nu-parser", version = "0.92.2" }
nu-path = { path = "../nu-path", version = "0.104.1" } nu-path = { path = "../nu-path", version = "0.92.2" }
nu-protocol = { path = "../nu-protocol", version = "0.104.1", default-features = false } nu-protocol = { path = "../nu-protocol", version = "0.92.2" }
indexmap = { workspace = true } indexmap = { workspace = true }
miette = { workspace = true } miette = { workspace = true }
[dev-dependencies] [dev-dependencies]

View File

@ -1,5 +0,0 @@
Utilities used by the different `nu-command`/`nu-cmd-*` crates, should not contain any full `Command` implementations.
## Internal Nushell crate
This crate implements components of Nushell and is not designed to support plugin authors or other users directly.

View File

@ -1,61 +1,61 @@
use crate::util::get_guaranteed_cwd;
use miette::Result; use miette::Result;
use nu_engine::{eval_block, eval_block_with_early_return}; use nu_engine::{eval_block, eval_block_with_early_return};
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::{ use nu_protocol::{
cli_error::{report_parse_error, report_shell_error}, cli_error::{report_error, report_error_new},
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{Closure, EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId, BlockId, PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId,
}; };
use std::{collections::HashMap, sync::Arc}; use std::sync::Arc;
pub fn eval_env_change_hook( pub fn eval_env_change_hook(
env_change_hook: &HashMap<String, Vec<Value>>, env_change_hook: Option<Value>,
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
for (env, hooks) in env_change_hook { if let Some(hook) = env_change_hook {
let before = engine_state.previous_env_vars.get(env); match hook {
let after = stack.get_env_var(engine_state, env); Value::Record { val, .. } => {
if before != after { for (env_name, hook_value) in &*val {
let before = before.cloned().unwrap_or_default(); let before = engine_state
let after = after.cloned().unwrap_or_default(); .previous_env_vars
.get(env_name)
.cloned()
.unwrap_or_default();
eval_hooks( let after = stack
engine_state, .get_env_var(engine_state, env_name)
stack, .unwrap_or_default();
vec![("$before".into(), before), ("$after".into(), after.clone())],
hooks,
"env_change",
)?;
Arc::make_mut(&mut engine_state.previous_env_vars).insert(env.clone(), after); if before != after {
eval_hook(
engine_state,
stack,
None,
vec![("$before".into(), before), ("$after".into(), after.clone())],
hook_value,
"env_change",
)?;
Arc::make_mut(&mut engine_state.previous_env_vars)
.insert(env_name.to_string(), after);
}
}
}
x => {
return Err(ShellError::TypeMismatch {
err_message: "record for the 'env_change' hook".to_string(),
span: x.span(),
});
}
} }
} }
Ok(()) Ok(())
} }
pub fn eval_hooks(
engine_state: &mut EngineState,
stack: &mut Stack,
arguments: Vec<(String, Value)>,
hooks: &[Value],
hook_name: &str,
) -> Result<(), ShellError> {
for hook in hooks {
eval_hook(
engine_state,
stack,
None,
arguments.clone(),
hook,
&format!("{hook_name} list, recursive"),
)?;
}
Ok(())
}
pub fn eval_hook( pub fn eval_hook(
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
@ -91,13 +91,12 @@ pub fn eval_hook(
false, false,
); );
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
report_parse_error(&working_set, err); report_error(&working_set, err);
return Err(ShellError::GenericError {
error: format!("Failed to run {hook_name} hook"), return Err(ShellError::UnsupportedConfigValue {
msg: "source code has errors".into(), expected: "valid source code".into(),
span: Some(span), value: "source code with syntax errors".into(),
help: None, span,
inner: Vec::new(),
}); });
} }
@ -124,7 +123,7 @@ pub fn eval_hook(
output = pipeline_data; output = pipeline_data;
} }
Err(err) => { Err(err) => {
report_shell_error(engine_state, &err); report_error_new(engine_state, &err);
} }
} }
@ -133,7 +132,16 @@ pub fn eval_hook(
} }
} }
Value::List { vals, .. } => { Value::List { vals, .. } => {
eval_hooks(engine_state, stack, arguments, vals, hook_name)?; for val in vals {
eval_hook(
engine_state,
stack,
None,
arguments.clone(),
val,
&format!("{hook_name} list, recursive"),
)?;
}
} }
Value::Record { val, .. } => { Value::Record { val, .. } => {
// Hooks can optionally be a record in this form: // Hooks can optionally be a record in this form:
@ -145,11 +153,11 @@ pub fn eval_hook(
// If it returns true (the default if a condition block is not specified), the hook should be run. // If it returns true (the default if a condition block is not specified), the hook should be run.
let do_run_hook = if let Some(condition) = val.get("condition") { let do_run_hook = if let Some(condition) = val.get("condition") {
let other_span = condition.span(); let other_span = condition.span();
if let Ok(closure) = condition.as_closure() { if let Ok(block_id) = condition.coerce_block() {
match run_hook( match run_hook_block(
engine_state, engine_state,
stack, stack,
closure, block_id,
None, None,
arguments.clone(), arguments.clone(),
other_span, other_span,
@ -159,10 +167,10 @@ pub fn eval_hook(
{ {
val val
} else { } else {
return Err(ShellError::RuntimeTypeMismatch { return Err(ShellError::UnsupportedConfigValue {
expected: Type::Bool, expected: "boolean output".to_string(),
actual: pipeline_data.get_type(), value: "other PipelineData variant".to_string(),
span: pipeline_data.span().unwrap_or(other_span), span: other_span,
}); });
} }
} }
@ -171,9 +179,9 @@ pub fn eval_hook(
} }
} }
} else { } else {
return Err(ShellError::RuntimeTypeMismatch { return Err(ShellError::UnsupportedConfigValue {
expected: Type::Closure, expected: "block".to_string(),
actual: condition.get_type(), value: format!("{}", condition.get_type()),
span: other_span, span: other_span,
}); });
} }
@ -186,7 +194,7 @@ pub fn eval_hook(
let Some(follow) = val.get("code") else { let Some(follow) = val.get("code") else {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: "code".into(), col_name: "code".into(),
span: Some(span), span,
src_span: span, src_span: span,
}); });
}; };
@ -215,13 +223,12 @@ pub fn eval_hook(
false, false,
); );
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
report_parse_error(&working_set, err); report_error(&working_set, err);
return Err(ShellError::GenericError {
error: format!("Failed to run {hook_name} hook"), return Err(ShellError::UnsupportedConfigValue {
msg: "source code has errors".into(), expected: "valid source code".into(),
span: Some(span), value: "source code with syntax errors".into(),
help: None, span: source_span,
inner: Vec::new(),
}); });
} }
@ -244,7 +251,7 @@ pub fn eval_hook(
output = pipeline_data; output = pipeline_data;
} }
Err(err) => { Err(err) => {
report_shell_error(engine_state, &err); report_error_new(engine_state, &err);
} }
} }
@ -252,50 +259,71 @@ pub fn eval_hook(
stack.remove_var(*var_id); stack.remove_var(*var_id);
} }
} }
Value::Block { val: block_id, .. } => {
run_hook_block(
engine_state,
stack,
*block_id,
input,
arguments,
source_span,
)?;
}
Value::Closure { val, .. } => { Value::Closure { val, .. } => {
run_hook(engine_state, stack, val, input, arguments, source_span)?; run_hook_block(
engine_state,
stack,
val.block_id,
input,
arguments,
source_span,
)?;
} }
other => { other => {
return Err(ShellError::RuntimeTypeMismatch { return Err(ShellError::UnsupportedConfigValue {
expected: Type::custom("string or closure"), expected: "block or string".to_string(),
actual: other.get_type(), value: format!("{}", other.get_type()),
span: source_span, span: source_span,
}); });
} }
} }
} }
} }
Value::Block { val: block_id, .. } => {
output = run_hook_block(engine_state, stack, *block_id, input, arguments, span)?;
}
Value::Closure { val, .. } => { Value::Closure { val, .. } => {
output = run_hook(engine_state, stack, val, input, arguments, span)?; output = run_hook_block(engine_state, stack, val.block_id, input, arguments, span)?;
} }
other => { other => {
return Err(ShellError::RuntimeTypeMismatch { return Err(ShellError::UnsupportedConfigValue {
expected: Type::custom("string, closure, record, or list"), expected: "string, block, record, or list of commands".into(),
actual: other.get_type(), value: format!("{}", other.get_type()),
span: other.span(), span: other.span(),
}); });
} }
} }
engine_state.merge_env(stack)?; let cwd = get_guaranteed_cwd(engine_state, stack);
engine_state.merge_env(stack, cwd)?;
Ok(output) Ok(output)
} }
fn run_hook( fn run_hook_block(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
closure: &Closure, block_id: BlockId,
optional_input: Option<PipelineData>, optional_input: Option<PipelineData>,
arguments: Vec<(String, Value)>, arguments: Vec<(String, Value)>,
span: Span, span: Span,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let block = engine_state.get_block(closure.block_id); let block = engine_state.get_block(block_id);
let input = optional_input.unwrap_or_else(PipelineData::empty); let input = optional_input.unwrap_or_else(PipelineData::empty);
let mut callee_stack = stack let mut callee_stack = stack
.captures_to_stack_preserve_out_dest(closure.captures.clone()) .gather_captures(engine_state, &block.captures)
.reset_pipes(); .reset_pipes();
for (idx, PositionalArg { var_id, .. }) in for (idx, PositionalArg { var_id, .. }) in

View File

@ -1,5 +1,5 @@
use nu_protocol::{ast::CellPath, PipelineData, ShellError, Signals, Span, Value}; use nu_protocol::{ast::CellPath, PipelineData, ShellError, Span, Value};
use std::sync::Arc; use std::sync::{atomic::AtomicBool, Arc};
pub trait CmdArgument { pub trait CmdArgument {
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>>; fn take_cell_paths(&mut self) -> Option<Vec<CellPath>>;
@ -40,7 +40,7 @@ pub fn operate<C, A>(
mut arg: A, mut arg: A,
input: PipelineData, input: PipelineData,
span: Span, span: Span,
signals: &Signals, ctrlc: Option<Arc<AtomicBool>>,
) -> Result<PipelineData, ShellError> ) -> Result<PipelineData, ShellError>
where where
A: CmdArgument + Send + Sync + 'static, A: CmdArgument + Send + Sync + 'static,
@ -55,7 +55,7 @@ where
_ => cmd(&v, &arg, span), _ => cmd(&v, &arg, span),
} }
}, },
signals, ctrlc,
), ),
Some(column_paths) => { Some(column_paths) => {
let arg = Arc::new(arg); let arg = Arc::new(arg);
@ -79,7 +79,7 @@ where
} }
v v
}, },
signals, ctrlc,
) )
} }
} }

View File

@ -1,4 +1,3 @@
#![doc = include_str!("../README.md")]
pub mod formats; pub mod formats;
pub mod hook; pub mod hook;
pub mod input_handler; pub mod input_handler;

View File

@ -1,28 +1,58 @@
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, ast::RangeInclusion,
Range, ShellError, Span, Value, engine::{EngineState, Stack, StateWorkingSet},
report_error, Range, ShellError, Span, Value,
}; };
use std::ops::Bound; use std::path::PathBuf;
pub fn get_init_cwd() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| {
std::env::var("PWD")
.map(Into::into)
.unwrap_or_else(|_| nu_path::home_dir().unwrap_or_default())
})
}
pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf {
nu_engine::env::current_dir(engine_state, stack).unwrap_or_else(|e| {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
crate::util::get_init_cwd()
})
}
type MakeRangeError = fn(&str, Span) -> ShellError; type MakeRangeError = fn(&str, Span) -> ShellError;
/// Returns a inclusive pair of boundary in given `range`.
pub fn process_range(range: &Range) -> Result<(isize, isize), MakeRangeError> { pub fn process_range(range: &Range) -> Result<(isize, isize), MakeRangeError> {
match range { let start = match &range.from {
Range::IntRange(range) => { Value::Int { val, .. } => isize::try_from(*val).unwrap_or_default(),
let start = range.start().try_into().unwrap_or(0); Value::Nothing { .. } => 0,
let end = match range.end() { _ => {
Bound::Included(v) => v as isize, return Err(|msg, span| ShellError::TypeMismatch {
Bound::Excluded(v) => (v - 1) as isize, err_message: msg.to_string(),
Bound::Unbounded => isize::MAX, span,
}; })
Ok((start, end))
} }
Range::FloatRange(_) => Err(|msg, span| ShellError::TypeMismatch { };
err_message: msg.to_string(),
span, let end = match &range.to {
}), Value::Int { val, .. } => {
} if matches!(range.inclusion, RangeInclusion::Inclusive) {
isize::try_from(*val).unwrap_or(isize::max_value())
} else {
isize::try_from(*val).unwrap_or(isize::max_value()) - 1
}
}
Value::Nothing { .. } => isize::max_value(),
_ => {
return Err(|msg, span| ShellError::TypeMismatch {
err_message: msg.to_string(),
span,
})
}
};
Ok((start, end))
} }
const HELP_MSG: &str = "Nushell's config file can be found with the command: $nu.config-path. \ const HELP_MSG: &str = "Nushell's config file can be found with the command: $nu.config-path. \
@ -78,10 +108,10 @@ pub fn get_editor(
get_editor_commandline(&config.buffer_editor, "$env.config.buffer_editor") get_editor_commandline(&config.buffer_editor, "$env.config.buffer_editor")
{ {
Ok(buff_editor) Ok(buff_editor)
} else if let Some(value) = env_vars.get("VISUAL") {
get_editor_commandline(value, "$env.VISUAL")
} else if let Some(value) = env_vars.get("EDITOR") { } else if let Some(value) = env_vars.get("EDITOR") {
get_editor_commandline(value, "$env.EDITOR") get_editor_commandline(value, "$env.EDITOR")
} else if let Some(value) = env_vars.get("VISUAL") {
get_editor_commandline(value, "$env.VISUAL")
} else { } else {
Err(ShellError::GenericError { Err(ShellError::GenericError {
error: "No editor configured".into(), error: "No editor configured".into(),

View File

@ -0,0 +1,75 @@
[package]
authors = ["The Nushell Project Developers"]
description = "Nushell's dataframe commands based on polars."
edition = "2021"
license = "MIT"
name = "nu-cmd-dataframe"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-dataframe"
version = "0.92.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
bench = false
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.92.2" }
nu-parser = { path = "../nu-parser", version = "0.92.2" }
nu-protocol = { path = "../nu-protocol", version = "0.92.2" }
# Potential dependencies for extras
chrono = { workspace = true, features = ["std", "unstable-locales"], default-features = false }
chrono-tz = { workspace = true }
fancy-regex = { workspace = true }
indexmap = { workspace = true }
num = { version = "0.4", optional = true }
serde = { workspace = true, features = ["derive"] }
# keep sqlparser at 0.39.0 until we can update polars
sqlparser = { version = "0.39.0", optional = true }
polars-io = { version = "0.37", features = ["avro"], optional = true }
polars-arrow = { version = "0.37", optional = true }
polars-ops = { version = "0.37", optional = true }
polars-plan = { version = "0.37", features = ["regex"], optional = true }
polars-utils = { version = "0.37", optional = true }
[dependencies.polars]
features = [
"arg_where",
"checked_arithmetic",
"concat_str",
"cross_join",
"csv",
"cum_agg",
"dtype-categorical",
"dtype-datetime",
"dtype-struct",
"dtype-i8",
"dtype-i16",
"dtype-u8",
"dtype-u16",
"dynamic_group_by",
"ipc",
"is_in",
"json",
"lazy",
"object",
"parquet",
"random",
"rolling_window",
"rows",
"serde",
"serde-lazy",
"strings",
"temporal",
"to_dummies",
]
default-features = false
optional = true
version = "0.37"
[features]
dataframe = ["num", "polars", "polars-io", "polars-arrow", "polars-ops", "polars-plan", "polars-utils", "sqlparser"]
default = []
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.92.2" }

View File

@ -1,24 +1,22 @@
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use crate::dataframe::values::{Axis, Column, NuDataFrame};
use nu_protocol::{ use nu_engine::command_prelude::*;
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
Value,
};
use crate::{
values::{Axis, Column, CustomValueSupport, NuDataFrame},
PolarsPlugin,
};
#[derive(Clone)] #[derive(Clone)]
pub struct AppendDF; pub struct AppendDF;
impl PluginCommand for AppendDF { impl Command for AppendDF {
type Plugin = PolarsPlugin; fn name(&self) -> &str {
"dfr append"
}
fn usage(&self) -> &str {
"Appends a new dataframe."
}
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build(self.name()) Signature::build(self.name())
.required("other", SyntaxShape::Any, "other dataframe to append") .required("other", SyntaxShape::Any, "dataframe to be appended")
.switch("col", "append as new columns instead of rows", Some('c')) .switch("col", "appends in col orientation", Some('c'))
.input_output_type( .input_output_type(
Type::Custom("dataframe".into()), Type::Custom("dataframe".into()),
Type::Custom("dataframe".into()), Type::Custom("dataframe".into()),
@ -26,30 +24,12 @@ impl PluginCommand for AppendDF {
.category(Category::Custom("dataframe".into())) .category(Category::Custom("dataframe".into()))
} }
fn run(
&self,
plugin: &Self::Plugin,
engine: &EngineInterface,
call: &EvaluatedCall,
input: PipelineData,
) -> Result<PipelineData, LabeledError> {
command(plugin, engine, call, input).map_err(LabeledError::from)
}
fn name(&self) -> &str {
"polars append"
}
fn description(&self) -> &str {
"Appends a new dataframe."
}
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
description: "Appends a dataframe as new columns", description: "Appends a dataframe as new columns",
example: r#"let a = ([[a b]; [1 2] [3 4]] | polars into-df); example: r#"let a = ([[a b]; [1 2] [3 4]] | dfr into-df);
$a | polars append $a"#, $a | dfr append $a"#,
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![ vec![
@ -78,7 +58,8 @@ impl PluginCommand for AppendDF {
}, },
Example { Example {
description: "Appends a dataframe merging at the end of columns", description: "Appends a dataframe merging at the end of columns",
example: r#"let a = ([[a b]; [1 2] [3 4]] | polars into-df); $a | polars append $a --col"#, example: r#"let a = ([[a b]; [1 2] [3 4]] | dfr into-df);
$a | dfr append $a --col"#,
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![ vec![
@ -109,36 +90,45 @@ impl PluginCommand for AppendDF {
}, },
] ]
} }
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
command(engine_state, stack, call, input)
}
} }
fn command( fn command(
plugin: &PolarsPlugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let other: Value = call.req(0)?; let other: Value = call.req(engine_state, stack, 0)?;
let axis = if call.has_flag("col")? { let axis = if call.has_flag(engine_state, stack, "col")? {
Axis::Column Axis::Column
} else { } else {
Axis::Row Axis::Row
}; };
let df_other = NuDataFrame::try_from_value(other)?;
let df = NuDataFrame::try_from_pipeline(input, call.head)?;
let df_other = NuDataFrame::try_from_value_coerce(plugin, &other, call.head)?; df.append_df(&df_other, axis, call.head)
let df = NuDataFrame::try_from_pipeline_coerce(plugin, input, call.head)?; .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None))
let df = df.append_df(&df_other, axis, call.head)?;
df.to_pipeline_data(plugin, engine, call.head)
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::super::test_dataframe::test_dataframe;
use super::*; use super::*;
use crate::test::test_polars_plugin_command;
#[test] #[test]
fn test_examples() -> Result<(), ShellError> { fn test_examples() {
test_polars_plugin_command(&AppendDF) test_dataframe(vec![Box::new(AppendDF {})])
} }
} }

View File

@ -1,28 +1,17 @@
use crate::{ use crate::dataframe::values::{str_to_dtype, NuDataFrame, NuExpression, NuLazyFrame};
dataframe::values::{str_to_dtype, NuExpression, NuLazyFrame}, use nu_engine::command_prelude::*;
values::{cant_convert_err, CustomValueSupport, PolarsPluginObject, PolarsPluginType},
PolarsPlugin,
};
use crate::values::NuDataFrame;
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{
record, Category, Example, LabeledError, PipelineData, ShellError, Signature, Span,
SyntaxShape, Type, Value,
};
use polars::prelude::*; use polars::prelude::*;
#[derive(Clone)] #[derive(Clone)]
pub struct CastDF; pub struct CastDF;
impl PluginCommand for CastDF { impl Command for CastDF {
type Plugin = PolarsPlugin;
fn name(&self) -> &str { fn name(&self) -> &str {
"polars cast" "dfr cast"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Cast a column to a different dtype." "Cast a column to a different dtype."
} }
@ -55,7 +44,7 @@ impl PluginCommand for CastDF {
vec![ vec![
Example { Example {
description: "Cast a column in a dataframe to a different dtype", description: "Cast a column in a dataframe to a different dtype",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars cast u8 a | polars schema", example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr cast u8 a | dfr schema",
result: Some(Value::record( result: Some(Value::record(
record! { record! {
"a" => Value::string("u8", Span::test_data()), "a" => Value::string("u8", Span::test_data()),
@ -66,8 +55,7 @@ impl PluginCommand for CastDF {
}, },
Example { Example {
description: "Cast a column in a lazy dataframe to a different dtype", description: "Cast a column in a lazy dataframe to a different dtype",
example: example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr into-lazy | dfr cast u8 a | dfr schema",
"[[a b]; [1 2] [3 4]] | polars into-df | polars into-lazy | polars cast u8 a | polars schema",
result: Some(Value::record( result: Some(Value::record(
record! { record! {
"a" => Value::string("u8", Span::test_data()), "a" => Value::string("u8", Span::test_data()),
@ -78,85 +66,90 @@ impl PluginCommand for CastDF {
}, },
Example { Example {
description: "Cast a column in a expression to a different dtype", description: "Cast a column in a expression to a different dtype",
example: r#"[[a b]; [1 2] [1 4]] | polars into-df | polars group-by a | polars agg [ (polars col b | polars cast u8 | polars min | polars as "b_min") ] | polars schema"#, example: r#"[[a b]; [1 2] [1 4]] | dfr into-df | dfr group-by a | dfr agg [ (dfr col b | dfr cast u8 | dfr min | dfr as "b_min") ] | dfr schema"#,
result: None, result: None
}, }
] ]
} }
fn run( fn run(
&self, &self,
plugin: &Self::Plugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, LabeledError> { ) -> Result<PipelineData, ShellError> {
let value = input.into_value(call.head)?; let value = input.into_value(call.head);
match PolarsPluginObject::try_from_value(plugin, &value)? { if NuLazyFrame::can_downcast(&value) {
PolarsPluginObject::NuLazyFrame(lazy) => { let (dtype, column_nm) = df_args(engine_state, stack, call)?;
let (dtype, column_nm) = df_args(call)?; let df = NuLazyFrame::try_from_value(value)?;
command_lazy(plugin, engine, call, column_nm, dtype, lazy) command_lazy(call, column_nm, dtype, df)
} } else if NuDataFrame::can_downcast(&value) {
PolarsPluginObject::NuDataFrame(df) => { let (dtype, column_nm) = df_args(engine_state, stack, call)?;
let (dtype, column_nm) = df_args(call)?; let df = NuDataFrame::try_from_value(value)?;
command_eager(plugin, engine, call, column_nm, dtype, df) command_eager(call, column_nm, dtype, df)
} } else {
PolarsPluginObject::NuExpression(expr) => { let dtype: String = call.req(engine_state, stack, 0)?;
let dtype: String = call.req(0)?; let dtype = str_to_dtype(&dtype, call.head)?;
let dtype = str_to_dtype(&dtype, call.head)?;
let expr: NuExpression = expr.into_polars().cast(dtype).into(); let expr = NuExpression::try_from_value(value)?;
expr.to_pipeline_data(plugin, engine, call.head) let expr: NuExpression = expr.into_polars().cast(dtype).into();
}
_ => Err(cant_convert_err( Ok(PipelineData::Value(
&value, NuExpression::into_value(expr, call.head),
&[ None,
PolarsPluginType::NuDataFrame, ))
PolarsPluginType::NuLazyFrame,
PolarsPluginType::NuExpression,
],
)),
} }
.map_err(LabeledError::from)
} }
} }
fn df_args(call: &EvaluatedCall) -> Result<(DataType, String), ShellError> { fn df_args(
let dtype = dtype_arg(call)?; engine_state: &EngineState,
let column_nm: String = call.opt(1)?.ok_or(ShellError::MissingParameter { stack: &mut Stack,
param_name: "column_name".into(), call: &Call,
span: call.head, ) -> Result<(DataType, String), ShellError> {
})?; let dtype = dtype_arg(engine_state, stack, call)?;
let column_nm: String =
call.opt(engine_state, stack, 1)?
.ok_or(ShellError::MissingParameter {
param_name: "column_name".into(),
span: call.head,
})?;
Ok((dtype, column_nm)) Ok((dtype, column_nm))
} }
fn dtype_arg(call: &EvaluatedCall) -> Result<DataType, ShellError> { fn dtype_arg(
let dtype: String = call.req(0)?; engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<DataType, ShellError> {
let dtype: String = call.req(engine_state, stack, 0)?;
str_to_dtype(&dtype, call.head) str_to_dtype(&dtype, call.head)
} }
fn command_lazy( fn command_lazy(
plugin: &PolarsPlugin, call: &Call,
engine: &EngineInterface,
call: &EvaluatedCall,
column_nm: String, column_nm: String,
dtype: DataType, dtype: DataType,
lazy: NuLazyFrame, lazy: NuLazyFrame,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let column = col(&column_nm).cast(dtype); let column = col(&column_nm).cast(dtype);
let lazy = lazy.to_polars().with_columns(&[column]); let lazy = lazy.into_polars().with_columns(&[column]);
let lazy = NuLazyFrame::new(false, lazy); let lazy = NuLazyFrame::new(false, lazy);
lazy.to_pipeline_data(plugin, engine, call.head)
Ok(PipelineData::Value(
NuLazyFrame::into_value(lazy, call.head)?,
None,
))
} }
fn command_eager( fn command_eager(
plugin: &PolarsPlugin, call: &Call,
engine: &EngineInterface,
call: &EvaluatedCall,
column_nm: String, column_nm: String,
dtype: DataType, dtype: DataType,
nu_df: NuDataFrame, nu_df: NuDataFrame,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let mut df = (*nu_df.df).clone(); let mut df = nu_df.df;
let column = df let column = df
.column(&column_nm) .column(&column_nm)
.map_err(|e| ShellError::GenericError { .map_err(|e| ShellError::GenericError {
@ -186,17 +179,17 @@ fn command_eager(
})?; })?;
let df = NuDataFrame::new(false, df); let df = NuDataFrame::new(false, df);
df.to_pipeline_data(plugin, engine, call.head) Ok(PipelineData::Value(df.into_value(call.head), None))
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::super::test_dataframe::test_dataframe;
use super::*; use super::*;
use crate::test::test_polars_plugin_command;
#[test] #[test]
fn test_examples() -> Result<(), ShellError> { fn test_examples() {
test_polars_plugin_command(&CastDF) test_dataframe(vec![Box::new(CastDF {})])
} }
} }

View File

@ -1,22 +1,15 @@
use crate::values::NuDataFrame; use crate::dataframe::values::NuDataFrame;
use crate::PolarsPlugin; use nu_engine::command_prelude::*;
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, Type, Value,
};
#[derive(Clone)] #[derive(Clone)]
pub struct ColumnsDF; pub struct ColumnsDF;
impl PluginCommand for ColumnsDF { impl Command for ColumnsDF {
type Plugin = PolarsPlugin;
fn name(&self) -> &str { fn name(&self) -> &str {
"polars columns" "dfr columns"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Show dataframe columns." "Show dataframe columns."
} }
@ -29,7 +22,7 @@ impl PluginCommand for ColumnsDF {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![Example { vec![Example {
description: "Dataframe columns", description: "Dataframe columns",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars columns", example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr columns",
result: Some(Value::list( result: Some(Value::list(
vec![Value::test_string("a"), Value::test_string("b")], vec![Value::test_string("a"), Value::test_string("b")],
Span::test_data(), Span::test_data(),
@ -39,27 +32,28 @@ impl PluginCommand for ColumnsDF {
fn run( fn run(
&self, &self,
plugin: &Self::Plugin, engine_state: &EngineState,
_engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, LabeledError> { ) -> Result<PipelineData, ShellError> {
command(plugin, call, input).map_err(|e| e.into()) command(engine_state, stack, call, input)
} }
} }
fn command( fn command(
plugin: &PolarsPlugin, _engine_state: &EngineState,
call: &EvaluatedCall, _stack: &mut Stack,
call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let df = NuDataFrame::try_from_pipeline_coerce(plugin, input, call.head)?; let df = NuDataFrame::try_from_pipeline(input, call.head)?;
let names: Vec<Value> = df let names: Vec<Value> = df
.as_ref() .as_ref()
.get_column_names() .get_column_names()
.iter() .iter()
.map(|v| Value::string(v.as_str(), call.head)) .map(|v| Value::string(*v, call.head))
.collect(); .collect();
let names = Value::list(names, call.head); let names = Value::list(names, call.head);
@ -69,11 +63,11 @@ fn command(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::super::test_dataframe::test_dataframe;
use super::*; use super::*;
use crate::test::test_polars_plugin_command;
#[test] #[test]
fn test_examples() -> Result<(), ShellError> { fn test_examples() {
test_polars_plugin_command(&ColumnsDF) test_dataframe(vec![Box::new(ColumnsDF {})])
} }
} }

View File

@ -1,26 +1,15 @@
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use crate::dataframe::values::{utils::convert_columns, Column, NuDataFrame};
use nu_protocol::{ use nu_engine::command_prelude::*;
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
Value,
};
use crate::values::CustomValueSupport;
use crate::PolarsPlugin;
use crate::values::utils::convert_columns;
use crate::values::{Column, NuDataFrame};
#[derive(Clone)] #[derive(Clone)]
pub struct DropDF; pub struct DropDF;
impl PluginCommand for DropDF { impl Command for DropDF {
type Plugin = PolarsPlugin;
fn name(&self) -> &str { fn name(&self) -> &str {
"polars drop" "dfr drop"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Creates a new dataframe by dropping the selected columns." "Creates a new dataframe by dropping the selected columns."
} }
@ -37,7 +26,7 @@ impl PluginCommand for DropDF {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![Example { vec![Example {
description: "drop column a", description: "drop column a",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars drop a", example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr drop a",
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![Column::new( vec![Column::new(
@ -54,25 +43,25 @@ impl PluginCommand for DropDF {
fn run( fn run(
&self, &self,
plugin: &Self::Plugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, LabeledError> { ) -> Result<PipelineData, ShellError> {
command(plugin, engine, call, input).map_err(LabeledError::from) command(engine_state, stack, call, input)
} }
} }
fn command( fn command(
plugin: &PolarsPlugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let columns: Vec<Value> = call.rest(0)?; let columns: Vec<Value> = call.rest(engine_state, stack, 0)?;
let (col_string, col_span) = convert_columns(columns, call.head)?; let (col_string, col_span) = convert_columns(columns, call.head)?;
let df = NuDataFrame::try_from_pipeline_coerce(plugin, input, call.head)?; let df = NuDataFrame::try_from_pipeline(input, call.head)?;
let new_df = col_string let new_df = col_string
.first() .first()
@ -97,30 +86,30 @@ fn command(
// If there are more columns in the drop selection list, these // If there are more columns in the drop selection list, these
// are added from the resulting dataframe // are added from the resulting dataframe
let polars_df = col_string.iter().skip(1).try_fold(new_df, |new_df, col| { col_string
new_df .iter()
.drop(&col.item) .skip(1)
.map_err(|e| ShellError::GenericError { .try_fold(new_df, |new_df, col| {
error: "Error dropping column".into(), new_df
msg: e.to_string(), .drop(&col.item)
span: Some(col.span), .map_err(|e| ShellError::GenericError {
help: None, error: "Error dropping column".into(),
inner: vec![], msg: e.to_string(),
}) span: Some(col.span),
})?; help: None,
inner: vec![],
let final_df = NuDataFrame::new(df.from_lazy, polars_df); })
})
final_df.to_pipeline_data(plugin, engine, call.head) .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None))
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::super::test_dataframe::test_dataframe;
use super::*; use super::*;
use crate::test::test_polars_plugin_command;
#[test] #[test]
fn test_examples() -> Result<(), ShellError> { fn test_examples() {
test_polars_plugin_command(&DropDF) test_dataframe(vec![Box::new(DropDF {})])
} }
} }

View File

@ -1,28 +1,17 @@
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use crate::dataframe::values::{utils::convert_columns_string, Column, NuDataFrame};
use nu_protocol::{ use nu_engine::command_prelude::*;
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
Value,
};
use polars::df;
use polars::prelude::UniqueKeepStrategy; use polars::prelude::UniqueKeepStrategy;
use crate::values::CustomValueSupport;
use crate::PolarsPlugin;
use crate::values::utils::convert_columns_string;
use crate::values::NuDataFrame;
#[derive(Clone)] #[derive(Clone)]
pub struct DropDuplicates; pub struct DropDuplicates;
impl PluginCommand for DropDuplicates { impl Command for DropDuplicates {
type Plugin = PolarsPlugin;
fn name(&self) -> &str { fn name(&self) -> &str {
"polars drop-duplicates" "dfr drop-duplicates"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Drops duplicate values in dataframe." "Drops duplicate values in dataframe."
} }
@ -49,17 +38,22 @@ impl PluginCommand for DropDuplicates {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![Example { vec![Example {
description: "drop duplicates", description: "drop duplicates",
example: "[[a b]; [1 2] [3 4] [1 2]] | polars into-df example: "[[a b]; [1 2] [3 4] [1 2]] | dfr into-df | dfr drop-duplicates",
| polars drop-duplicates
| polars sort-by a",
result: Some( result: Some(
NuDataFrame::from( NuDataFrame::try_from_columns(
df!( vec![
"a" => &[1i64, 3], Column::new(
"b" => &[2i64, 4], "a".to_string(),
) vec![Value::test_int(3), Value::test_int(1)],
.expect("should not fail"), ),
Column::new(
"b".to_string(),
vec![Value::test_int(4), Value::test_int(2)],
),
],
None,
) )
.expect("simple df for test should not fail")
.into_value(Span::test_data()), .into_value(Span::test_data()),
), ),
}] }]
@ -67,22 +61,22 @@ impl PluginCommand for DropDuplicates {
fn run( fn run(
&self, &self,
plugin: &Self::Plugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, LabeledError> { ) -> Result<PipelineData, ShellError> {
command(plugin, engine, call, input).map_err(LabeledError::from) command(engine_state, stack, call, input)
} }
} }
fn command( fn command(
plugin: &PolarsPlugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let columns: Option<Vec<Value>> = call.opt(0)?; let columns: Option<Vec<Value>> = call.opt(engine_state, stack, 0)?;
let (subset, col_span) = match columns { let (subset, col_span) = match columns {
Some(cols) => { Some(cols) => {
let (agg_string, col_span) = convert_columns_string(cols, call.head)?; let (agg_string, col_span) = convert_columns_string(cols, call.head)?;
@ -91,39 +85,35 @@ fn command(
None => (None, call.head), None => (None, call.head),
}; };
let df = NuDataFrame::try_from_pipeline_coerce(plugin, input, call.head)?; let df = NuDataFrame::try_from_pipeline(input, call.head)?;
let subset_slice = subset.as_ref().map(|cols| &cols[..]); let subset_slice = subset.as_ref().map(|cols| &cols[..]);
let keep_strategy = if call.has_flag("last")? { let keep_strategy = if call.has_flag(engine_state, stack, "last")? {
UniqueKeepStrategy::Last UniqueKeepStrategy::Last
} else { } else {
UniqueKeepStrategy::First UniqueKeepStrategy::First
}; };
let polars_df = df df.as_ref()
.as_ref() .unique(subset_slice, keep_strategy, None)
.unique_stable(subset_slice, keep_strategy, None)
.map_err(|e| ShellError::GenericError { .map_err(|e| ShellError::GenericError {
error: "Error dropping duplicates".into(), error: "Error dropping duplicates".into(),
msg: e.to_string(), msg: e.to_string(),
span: Some(col_span), span: Some(col_span),
help: None, help: None,
inner: vec![], inner: vec![],
})?; })
.map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None))
let df = NuDataFrame::new(df.from_lazy, polars_df);
df.to_pipeline_data(plugin, engine, call.head)
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::test::test_polars_plugin_command; use super::super::super::test_dataframe::test_dataframe;
use super::*; use super::*;
#[test] #[test]
fn test_examples() -> Result<(), ShellError> { fn test_examples() {
test_polars_plugin_command(&DropDuplicates) test_dataframe(vec![Box::new(DropDuplicates {})])
} }
} }

View File

@ -1,26 +1,15 @@
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use crate::dataframe::values::{utils::convert_columns_string, Column, NuDataFrame};
use nu_protocol::{ use nu_engine::command_prelude::*;
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
Value,
};
use crate::values::CustomValueSupport;
use crate::PolarsPlugin;
use crate::values::utils::convert_columns_string;
use crate::values::{Column, NuDataFrame};
#[derive(Clone)] #[derive(Clone)]
pub struct DropNulls; pub struct DropNulls;
impl PluginCommand for DropNulls { impl Command for DropNulls {
type Plugin = PolarsPlugin;
fn name(&self) -> &str { fn name(&self) -> &str {
"polars drop-nulls" "dfr drop-nulls"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Drops null values in dataframe." "Drops null values in dataframe."
} }
@ -42,10 +31,10 @@ impl PluginCommand for DropNulls {
vec![ vec![
Example { Example {
description: "drop null values in dataframe", description: "drop null values in dataframe",
example: r#"let df = ([[a b]; [1 2] [3 0] [1 2]] | polars into-df); example: r#"let df = ([[a b]; [1 2] [3 0] [1 2]] | dfr into-df);
let res = ($df.b / $df.b); let res = ($df.b / $df.b);
let a = ($df | polars with-column $res --name res); let a = ($df | dfr with-column $res --name res);
$a | polars drop-nulls"#, $a | dfr drop-nulls"#,
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![ vec![
@ -70,8 +59,8 @@ impl PluginCommand for DropNulls {
}, },
Example { Example {
description: "drop null values in dataframe", description: "drop null values in dataframe",
example: r#"let s = ([1 2 0 0 3 4] | polars into-df); example: r#"let s = ([1 2 0 0 3 4] | dfr into-df);
($s / $s) | polars drop-nulls"#, ($s / $s) | dfr drop-nulls"#,
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![Column::new( vec![Column::new(
@ -94,24 +83,24 @@ impl PluginCommand for DropNulls {
fn run( fn run(
&self, &self,
plugin: &Self::Plugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, LabeledError> { ) -> Result<PipelineData, ShellError> {
command(plugin, engine, call, input).map_err(LabeledError::from) command(engine_state, stack, call, input)
} }
} }
fn command( fn command(
plugin: &PolarsPlugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let df = NuDataFrame::try_from_pipeline_coerce(plugin, input, call.head)?; let df = NuDataFrame::try_from_pipeline(input, call.head)?;
let columns: Option<Vec<Value>> = call.opt(0)?; let columns: Option<Vec<Value>> = call.opt(engine_state, stack, 0)?;
let (subset, col_span) = match columns { let (subset, col_span) = match columns {
Some(cols) => { Some(cols) => {
@ -123,8 +112,7 @@ fn command(
let subset_slice = subset.as_ref().map(|cols| &cols[..]); let subset_slice = subset.as_ref().map(|cols| &cols[..]);
let polars_df = df df.as_ref()
.as_ref()
.drop_nulls(subset_slice) .drop_nulls(subset_slice)
.map_err(|e| ShellError::GenericError { .map_err(|e| ShellError::GenericError {
error: "Error dropping nulls".into(), error: "Error dropping nulls".into(),
@ -132,18 +120,18 @@ fn command(
span: Some(col_span), span: Some(col_span),
help: None, help: None,
inner: vec![], inner: vec![],
})?; })
let df = NuDataFrame::new(df.from_lazy, polars_df); .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None))
df.to_pipeline_data(plugin, engine, call.head)
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::super::test_dataframe::test_dataframe;
use super::super::WithColumn;
use super::*; use super::*;
use crate::test::test_polars_plugin_command;
#[test] #[test]
fn test_examples() -> Result<(), ShellError> { fn test_examples() {
test_polars_plugin_command(&DropNulls) test_dataframe(vec![Box::new(DropNulls {}), Box::new(WithColumn {})])
} }
} }

View File

@ -0,0 +1,104 @@
use crate::dataframe::values::{Column, NuDataFrame};
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct DataTypes;
impl Command for DataTypes {
fn name(&self) -> &str {
"dfr dtypes"
}
fn usage(&self) -> &str {
"Show dataframe data types."
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.input_output_type(
Type::Custom("dataframe".into()),
Type::Custom("dataframe".into()),
)
.category(Category::Custom("dataframe".into()))
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Dataframe dtypes",
example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr dtypes",
result: Some(
NuDataFrame::try_from_columns(
vec![
Column::new(
"column".to_string(),
vec![Value::test_string("a"), Value::test_string("b")],
),
Column::new(
"dtype".to_string(),
vec![Value::test_string("i64"), Value::test_string("i64")],
),
],
None,
)
.expect("simple df for test should not fail")
.into_value(Span::test_data()),
),
}]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
command(engine_state, stack, call, input)
}
}
fn command(
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let df = NuDataFrame::try_from_pipeline(input, call.head)?;
let mut dtypes: Vec<Value> = Vec::new();
let names: Vec<Value> = df
.as_ref()
.get_column_names()
.iter()
.map(|v| {
let dtype = df
.as_ref()
.column(v)
.expect("using name from list of names from dataframe")
.dtype();
let dtype_str = dtype.to_string();
dtypes.push(Value::string(dtype_str, call.head));
Value::string(*v, call.head)
})
.collect();
let names_col = Column::new("column".to_string(), names);
let dtypes_col = Column::new("dtype".to_string(), dtypes);
NuDataFrame::try_from_columns(vec![names_col, dtypes_col], None)
.map(|df| PipelineData::Value(df.into_value(call.head), None))
}
#[cfg(test)]
mod test {
use super::super::super::test_dataframe::test_dataframe;
use super::*;
#[test]
fn test_examples() {
test_dataframe(vec![Box::new(DataTypes {})])
}
}

View File

@ -0,0 +1,107 @@
use crate::dataframe::values::NuDataFrame;
use nu_engine::command_prelude::*;
use polars::{prelude::*, series::Series};
#[derive(Clone)]
pub struct Dummies;
impl Command for Dummies {
fn name(&self) -> &str {
"dfr dummies"
}
fn usage(&self) -> &str {
"Creates a new dataframe with dummy variables."
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.switch("drop-first", "Drop first row", Some('d'))
.input_output_type(
Type::Custom("dataframe".into()),
Type::Custom("dataframe".into()),
)
.category(Category::Custom("dataframe".into()))
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Create new dataframe with dummy variables from a dataframe",
example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr dummies",
result: Some(
NuDataFrame::try_from_series(
vec![
Series::new("a_1", &[1_u8, 0]),
Series::new("a_3", &[0_u8, 1]),
Series::new("b_2", &[1_u8, 0]),
Series::new("b_4", &[0_u8, 1]),
],
Span::test_data(),
)
.expect("simple df for test should not fail")
.into_value(Span::test_data()),
),
},
Example {
description: "Create new dataframe with dummy variables from a series",
example: "[1 2 2 3 3] | dfr into-df | dfr dummies",
result: Some(
NuDataFrame::try_from_series(
vec![
Series::new("0_1", &[1_u8, 0, 0, 0, 0]),
Series::new("0_2", &[0_u8, 1, 1, 0, 0]),
Series::new("0_3", &[0_u8, 0, 0, 1, 1]),
],
Span::test_data(),
)
.expect("simple df for test should not fail")
.into_value(Span::test_data()),
),
},
]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
command(engine_state, stack, call, input)
}
}
fn command(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let drop_first: bool = call.has_flag(engine_state, stack, "drop-first")?;
let df = NuDataFrame::try_from_pipeline(input, call.head)?;
df.as_ref()
.to_dummies(None, drop_first)
.map_err(|e| ShellError::GenericError {
error: "Error calculating dummies".into(),
msg: e.to_string(),
span: Some(call.head),
help: Some("The only allowed column types for dummies are String or Int".into()),
inner: vec![],
})
.map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None))
}
#[cfg(test)]
mod test {
use super::super::super::test_dataframe::test_dataframe;
use super::*;
#[test]
fn test_examples() {
test_dataframe(vec![Box::new(Dummies {})])
}
}

View File

@ -1,29 +1,17 @@
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use crate::dataframe::values::{Column, NuDataFrame, NuExpression, NuLazyFrame};
use nu_protocol::{ use nu_engine::command_prelude::*;
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
Value,
};
use polars::prelude::LazyFrame; use polars::prelude::LazyFrame;
use crate::{
dataframe::values::{NuExpression, NuLazyFrame},
values::{cant_convert_err, CustomValueSupport, PolarsPluginObject, PolarsPluginType},
PolarsPlugin,
};
use crate::values::{Column, NuDataFrame};
#[derive(Clone)] #[derive(Clone)]
pub struct FilterWith; pub struct FilterWith;
impl PluginCommand for FilterWith { impl Command for FilterWith {
type Plugin = PolarsPlugin;
fn name(&self) -> &str { fn name(&self) -> &str {
"polars filter-with" "dfr filter-with"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Filters dataframe using a mask or expression as reference." "Filters dataframe using a mask or expression as reference."
} }
@ -45,8 +33,8 @@ impl PluginCommand for FilterWith {
vec![ vec![
Example { Example {
description: "Filter dataframe using a bool mask", description: "Filter dataframe using a bool mask",
example: r#"let mask = ([true false] | polars into-df); example: r#"let mask = ([true false] | dfr into-df);
[[a b]; [1 2] [3 4]] | polars into-df | polars filter-with $mask"#, [[a b]; [1 2] [3 4]] | dfr into-df | dfr filter-with $mask"#,
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![ vec![
@ -61,7 +49,7 @@ impl PluginCommand for FilterWith {
}, },
Example { Example {
description: "Filter dataframe using an expression", description: "Filter dataframe using an expression",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars filter-with ((polars col a) > 1)", example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr filter-with ((dfr col a) > 1)",
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![ vec![
@ -79,42 +67,43 @@ impl PluginCommand for FilterWith {
fn run( fn run(
&self, &self,
plugin: &Self::Plugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, LabeledError> { ) -> Result<PipelineData, ShellError> {
let value = input.into_value(call.head)?; let value = input.into_value(call.head);
match PolarsPluginObject::try_from_value(plugin, &value)? {
PolarsPluginObject::NuDataFrame(df) => command_eager(plugin, engine, call, df), if NuLazyFrame::can_downcast(&value) {
PolarsPluginObject::NuLazyFrame(lazy) => command_lazy(plugin, engine, call, lazy), let df = NuLazyFrame::try_from_value(value)?;
_ => Err(cant_convert_err( command_lazy(engine_state, stack, call, df)
&value, } else {
&[PolarsPluginType::NuDataFrame, PolarsPluginType::NuLazyFrame], let df = NuDataFrame::try_from_value(value)?;
)), command_eager(engine_state, stack, call, df)
} }
.map_err(LabeledError::from)
} }
} }
fn command_eager( fn command_eager(
plugin: &PolarsPlugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
df: NuDataFrame, df: NuDataFrame,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let mask_value: Value = call.req(0)?; let mask_value: Value = call.req(engine_state, stack, 0)?;
let mask_span = mask_value.span(); let mask_span = mask_value.span();
if NuExpression::can_downcast(&mask_value) { if NuExpression::can_downcast(&mask_value) {
let expression = NuExpression::try_from_value(plugin, &mask_value)?; let expression = NuExpression::try_from_value(mask_value)?;
let lazy = df.lazy(); let lazy = NuLazyFrame::new(true, df.lazy());
let lazy = lazy.apply_with_expr(expression, LazyFrame::filter); let lazy = lazy.apply_with_expr(expression, LazyFrame::filter);
lazy.to_pipeline_data(plugin, engine, call.head) Ok(PipelineData::Value(
NuLazyFrame::into_value(lazy, call.head)?,
None,
))
} else { } else {
let mask = NuDataFrame::try_from_value_coerce(plugin, &mask_value, mask_span)? let mask = NuDataFrame::try_from_value(mask_value)?.as_series(mask_span)?;
.as_series(mask_span)?;
let mask = mask.bool().map_err(|e| ShellError::GenericError { let mask = mask.bool().map_err(|e| ShellError::GenericError {
error: "Error casting to bool".into(), error: "Error casting to bool".into(),
msg: e.to_string(), msg: e.to_string(),
@ -123,8 +112,7 @@ fn command_eager(
inner: vec![], inner: vec![],
})?; })?;
let polars_df = df df.as_ref()
.as_ref()
.filter(mask) .filter(mask)
.map_err(|e| ShellError::GenericError { .map_err(|e| ShellError::GenericError {
error: "Error filtering dataframe".into(), error: "Error filtering dataframe".into(),
@ -132,31 +120,36 @@ fn command_eager(
span: Some(call.head), span: Some(call.head),
help: Some("The only allowed column types for dummies are String or Int".into()), help: Some("The only allowed column types for dummies are String or Int".into()),
inner: vec![], inner: vec![],
})?; })
let df = NuDataFrame::new(df.from_lazy, polars_df); .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None))
df.to_pipeline_data(plugin, engine, call.head)
} }
} }
fn command_lazy( fn command_lazy(
plugin: &PolarsPlugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
lazy: NuLazyFrame, lazy: NuLazyFrame,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let expr: Value = call.req(0)?; let expr: Value = call.req(engine_state, stack, 0)?;
let expr = NuExpression::try_from_value(plugin, &expr)?; let expr = NuExpression::try_from_value(expr)?;
let lazy = lazy.apply_with_expr(expr, LazyFrame::filter); let lazy = lazy.apply_with_expr(expr, LazyFrame::filter);
lazy.to_pipeline_data(plugin, engine, call.head)
Ok(PipelineData::Value(
NuLazyFrame::into_value(lazy, call.head)?,
None,
))
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::super::test_dataframe::test_dataframe;
use super::*; use super::*;
use crate::test::test_polars_plugin_command; use crate::dataframe::expressions::ExprCol;
#[test] #[test]
fn test_examples() -> Result<(), ShellError> { fn test_examples() {
test_polars_plugin_command(&FilterWith) test_dataframe(vec![Box::new(FilterWith {}), Box::new(ExprCol {})])
} }
} }

View File

@ -1,26 +1,15 @@
use crate::{ use crate::dataframe::values::{Column, NuDataFrame, NuExpression};
values::{Column, CustomValueSupport, NuLazyFrame, PolarsPluginObject}, use nu_engine::command_prelude::*;
PolarsPlugin,
};
use crate::values::{NuDataFrame, NuExpression};
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
Value,
};
#[derive(Clone)] #[derive(Clone)]
pub struct FirstDF; pub struct FirstDF;
impl PluginCommand for FirstDF { impl Command for FirstDF {
type Plugin = PolarsPlugin;
fn name(&self) -> &str { fn name(&self) -> &str {
"polars first" "dfr first"
} }
fn description(&self) -> &str { fn usage(&self) -> &str {
"Show only the first number of rows or create a first expression" "Show only the first number of rows or create a first expression"
} }
@ -48,7 +37,7 @@ impl PluginCommand for FirstDF {
vec![ vec![
Example { Example {
description: "Return the first row of a dataframe", description: "Return the first row of a dataframe",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars first", example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr first",
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![ vec![
@ -63,7 +52,7 @@ impl PluginCommand for FirstDF {
}, },
Example { Example {
description: "Return the first two rows of a dataframe", description: "Return the first two rows of a dataframe",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars first 2", example: "[[a b]; [1 2] [3 4]] | dfr into-df | dfr first 2",
result: Some( result: Some(
NuDataFrame::try_from_columns( NuDataFrame::try_from_columns(
vec![ vec![
@ -84,7 +73,7 @@ impl PluginCommand for FirstDF {
}, },
Example { Example {
description: "Creates a first expression from a column", description: "Creates a first expression from a column",
example: "polars col a | polars first", example: "dfr col a | dfr first",
result: None, result: None,
}, },
] ]
@ -92,65 +81,64 @@ impl PluginCommand for FirstDF {
fn run( fn run(
&self, &self,
plugin: &Self::Plugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, LabeledError> { ) -> Result<PipelineData, ShellError> {
let value = input.into_value(call.head)?; let value = input.into_value(call.head);
match PolarsPluginObject::try_from_value(plugin, &value)? { if NuDataFrame::can_downcast(&value) {
PolarsPluginObject::NuDataFrame(df) => { let df = NuDataFrame::try_from_value(value)?;
command_eager(plugin, engine, call, df).map_err(|e| e.into()) command(engine_state, stack, call, df)
} } else {
PolarsPluginObject::NuLazyFrame(lazy) => { let expr = NuExpression::try_from_value(value)?;
command_lazy(plugin, engine, call, lazy).map_err(|e| e.into()) let expr: NuExpression = expr.into_polars().first().into();
}
_ => {
let expr = NuExpression::try_from_value(plugin, &value)?;
let expr: NuExpression = expr.into_polars().first().into();
expr.to_pipeline_data(plugin, engine, call.head) Ok(PipelineData::Value(
.map_err(LabeledError::from) NuExpression::into_value(expr, call.head),
} None,
))
} }
} }
} }
fn command_eager( fn command(
plugin: &PolarsPlugin, engine_state: &EngineState,
engine: &EngineInterface, stack: &mut Stack,
call: &EvaluatedCall, call: &Call,
df: NuDataFrame, df: NuDataFrame,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let rows: Option<usize> = call.opt(0)?; let rows: Option<usize> = call.opt(engine_state, stack, 0)?;
let rows = rows.unwrap_or(1); let rows = rows.unwrap_or(1);
let res = df.as_ref().head(Some(rows)); let res = df.as_ref().head(Some(rows));
let res = NuDataFrame::new(false, res); Ok(PipelineData::Value(
NuDataFrame::dataframe_into_value(res, call.head),
res.to_pipeline_data(plugin, engine, call.head) None,
} ))
fn command_lazy(
plugin: &PolarsPlugin,
engine: &EngineInterface,
call: &EvaluatedCall,
lazy: NuLazyFrame,
) -> Result<PipelineData, ShellError> {
let rows: Option<u64> = call.opt(0)?;
let rows = rows.unwrap_or(1);
let res: NuLazyFrame = lazy.to_polars().limit(rows).into();
res.to_pipeline_data(plugin, engine, call.head)
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::super::test_dataframe::{build_test_engine_state, test_dataframe_example};
use super::*; use super::*;
use crate::test::test_polars_plugin_command; use crate::dataframe::lazy::aggregate::LazyAggregate;
use crate::dataframe::lazy::groupby::ToLazyGroupBy;
#[test] #[test]
fn test_examples() -> Result<(), ShellError> { fn test_examples_dataframe() {
test_polars_plugin_command(&FirstDF) let mut engine_state = build_test_engine_state(vec![Box::new(FirstDF {})]);
test_dataframe_example(&mut engine_state, &FirstDF.examples()[0]);
test_dataframe_example(&mut engine_state, &FirstDF.examples()[1]);
}
#[test]
fn test_examples_expression() {
let mut engine_state = build_test_engine_state(vec![
Box::new(FirstDF {}),
Box::new(LazyAggregate {}),
Box::new(ToLazyGroupBy {}),
]);
test_dataframe_example(&mut engine_state, &FirstDF.examples()[2]);
} }
} }

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