diff --git a/nushell/.cargo/config.toml b/nushell/.cargo/config.toml new file mode 100644 index 0000000..ad1e8ce --- /dev/null +++ b/nushell/.cargo/config.toml @@ -0,0 +1,33 @@ +[target.x86_64-pc-windows-msvc] +# increase the default windows stack size +# statically link the CRT so users don't have to install it +rustflags = ["-C", "link-args=-stack:10000000", "-C", "target-feature=+crt-static"] + +# keeping this but commentting out in case we need them in the future + +# set a 2 gb stack size (0x80000000 = 2147483648 bytes = 2 GB) +# [target.x86_64-unknown-linux-gnu] +# rustflags = ["-C", "link-args=-Wl,-z stack-size=0x80000000"] + +# set a 2 gb stack size (0x80000000 = 2147483648 bytes = 2 GB) +# [target.x86_64-apple-darwin] +# rustflags = ["-C", "link-args=-Wl,-stack_size,0x80000000"] + +# How to use mold in linux and mac + +# [target.x86_64-unknown-linux-gnu] +# linker = "clang" +# rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/mold"] + +# [target.x86_64-apple-darwin] +# linker = "clang" +# rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +# [target.aarch64-apple-darwin] +# linker = "clang" +# rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[target.aarch64-apple-darwin] +# We can guarantee that this target will always run on a CPU with _at least_ +# these capabilities, so let's optimize for them +rustflags = ["-Ctarget-cpu=apple-m1"] \ No newline at end of file diff --git a/nushell/.gitattributes b/nushell/.gitattributes new file mode 100644 index 0000000..71fd44c --- /dev/null +++ b/nushell/.gitattributes @@ -0,0 +1,2 @@ +# Example of a `.gitattributes` file which reclassifies `.nu` files as Nushell: +*.nu linguist-language=Nushell diff --git a/nushell/.githooks/pre-commit b/nushell/.githooks/pre-commit new file mode 100755 index 0000000..f83086c --- /dev/null +++ b/nushell/.githooks/pre-commit @@ -0,0 +1,5 @@ +#!/usr/bin/env nu + +use ../toolkit.nu fmt + +fmt --check --verbose \ No newline at end of file diff --git a/nushell/.githooks/pre-push b/nushell/.githooks/pre-push new file mode 100755 index 0000000..0ba83ea --- /dev/null +++ b/nushell/.githooks/pre-push @@ -0,0 +1,6 @@ +#!/usr/bin/env nu + +use ../toolkit.nu [fmt, clippy] + +fmt --check --verbose +clippy --verbose \ No newline at end of file diff --git a/nushell/.github/AUTO_ISSUE_TEMPLATE/README.md b/nushell/.github/AUTO_ISSUE_TEMPLATE/README.md new file mode 100644 index 0000000..2d65248 --- /dev/null +++ b/nushell/.github/AUTO_ISSUE_TEMPLATE/README.md @@ -0,0 +1 @@ +This directory is intended for templates to automatically create issues with the [create-an-issue](https://github.com/JasonEtco/create-an-issue) action. diff --git a/nushell/.github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md b/nushell/.github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md new file mode 100644 index 0000000..5013e09 --- /dev/null +++ b/nushell/.github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md @@ -0,0 +1,16 @@ +--- +name: Nightly build of release binaries failed +about: Used to submit issues related to binaries release workflow +title: 'Attention: Nightly build of release binaries failed' +labels: ['build-package', 'priority'] +assignees: '' + +--- + +**Nightly build of release binaries failed** + +Hi there: + +If you see me here that means there is a release failure for the nightly build + +Please **click the status badge** to see more details: [![Nightly Build](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml/badge.svg)](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml) diff --git a/nushell/.github/ISSUE_TEMPLATE/bug_report.yml b/nushell/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7003641 --- /dev/null +++ b/nushell/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: Bug Report +description: Create a report to help us improve +labels: ["needs-triage"] +body: + - type: textarea + id: description + attributes: + label: Describe the bug + description: Thank you for your bug report. + validations: + required: true + - type: textarea + id: repro + attributes: + label: How to reproduce + description: Steps to reproduce the behavior (including succinct code examples or screenshots of the observed behavior) + placeholder: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + placeholder: I expected nu to... + validations: + required: true + - type: textarea + id: config + attributes: + label: Configuration + description: "Please run `version | transpose key value | to md --pretty` and paste the output to show OS, features, etc." + placeholder: | + > version | transpose key value | to md --pretty + | key | value | + | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | version | 0.40.0 | + | build_os | linux-x86_64 | + | rust_version | rustc 1.56.1 | + | cargo_version | cargo 1.56.0 | + | pkg_version | 0.40.0 | + | build_time | 1980-01-01 00:00:00 +00:00 | + | build_rust_channel | release | + | features | clipboard-cli, ctrlc, dataframe, default, rustyline, term, trash, uuid, which, zip | + | 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: + required: true diff --git a/nushell/.github/ISSUE_TEMPLATE/feature_request.yml b/nushell/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..7e192c6 --- /dev/null +++ b/nushell/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,35 @@ +name: Feature Request +description: "When you want a new feature for something that doesn't already exist" +labels: ["needs-triage", "enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Related problem + description: Thank you for your feature request. + placeholder: | + A clear and concise description of what the problem is. + Example: I am trying to do [...] but [...] + validations: + required: false + - type: textarea + id: desired + attributes: + label: "Describe the solution you'd like" + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: "Describe alternatives you've considered" + description: "A clear and concise description of any alternative solutions or features you've considered." + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional context and details + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/nushell/.github/ISSUE_TEMPLATE/question.yml b/nushell/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..6ed045b --- /dev/null +++ b/nushell/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,21 @@ +name: Question +description: "When you have a question to ask" +labels: "question" +body: + - type: textarea + id: problem + attributes: + label: Question + description: Leave your question here + placeholder: | + A clear and concise question + Example: Is there any equivalent of bash's $CDPATH in Nu? + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional context and details + description: Add any other context, screenshots or other media that will help us understand your question here, if needed. + validations: + required: false diff --git a/nushell/.github/dependabot.yml b/nushell/.github/dependabot.yml new file mode 100644 index 0000000..07e0b2b --- /dev/null +++ b/nushell/.github/dependabot.yml @@ -0,0 +1,40 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +# docs +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + # We release on Tuesdays and open dependabot PRs will rebase after the + # version bump and thus consume unnecessary workers during release, thus + # let's open new ones on Wednesday + day: "wednesday" + ignore: + - dependency-name: "*" + 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" + directory: "/" + schedule: + interval: "weekly" + day: "wednesday" diff --git a/nushell/.github/labeler.yml b/nushell/.github/labeler.yml new file mode 100644 index 0000000..cb500a3 --- /dev/null +++ b/nushell/.github/labeler.yml @@ -0,0 +1,40 @@ +# A bot for automatically labelling pull requests +# See https://github.com/actions/labeler + +dataframe: + - changed-files: + - any-glob-to-any-file: + - crates/nu_plugin_polars/** + +std-library: + - changed-files: + - any-glob-to-any-file: + - crates/nu-std/** + +ci: + - changed-files: + - any-glob-to-any-file: + - .github/workflows/** + + +LSP: + - changed-files: + - any-glob-to-any-file: + - crates/nu-lsp/** + +parser: + - changed-files: + - any-glob-to-any-file: + - crates/nu-parser/** + +pr:plugins: + - changed-files: + - any-glob-to-any-file: + # plugins API + - crates/nu-plugin/** + - crates/nu-plugin-core/** + - crates/nu-plugin-engine/** + - crates/nu-plugin-protocol/** + - crates/nu-plugin-test-support/** + # specific plugins (like polars) + - crates/nu_plugin_*/** diff --git a/nushell/.github/pull_request_template.md b/nushell/.github/pull_request_template.md new file mode 100644 index 0000000..23d6a5f --- /dev/null +++ b/nushell/.github/pull_request_template.md @@ -0,0 +1,40 @@ + + +# Description + + +# User-Facing Changes + + +# Tests + Formatting + + +# After Submitting + diff --git a/nushell/.github/workflows/audit.yml b/nushell/.github/workflows/audit.yml new file mode 100644 index 0000000..d6044d2 --- /dev/null +++ b/nushell/.github/workflows/audit.yml @@ -0,0 +1,25 @@ +name: Security audit +on: + pull_request: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + push: + branches: + - main + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CLICOLOR: 1 + +jobs: + security_audit: + runs-on: ubuntu-latest + # Prevent sudden announcement of a new advisory from failing ci: + continue-on-error: true + steps: + - uses: actions/checkout@v4.1.7 + - uses: rustsec/audit-check@v2.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/nushell/.github/workflows/beta-test.yml b/nushell/.github/workflows/beta-test.yml new file mode 100644 index 0000000..7770c6a --- /dev/null +++ b/nushell/.github/workflows/beta-test.yml @@ -0,0 +1,52 @@ +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 diff --git a/nushell/.github/workflows/check-msrv.nu b/nushell/.github/workflows/check-msrv.nu new file mode 100644 index 0000000..f0c92fe --- /dev/null +++ b/nushell/.github/workflows/check-msrv.nu @@ -0,0 +1,12 @@ +let toolchain_spec = open rust-toolchain.toml | get toolchain.channel +let msrv_spec = open Cargo.toml | get package.rust-version + +# This check is conservative in the sense that we use `rust-toolchain.toml`'s +# override to ensure that this is the upper-bound for the minimum supported +# rust version +if $toolchain_spec != $msrv_spec { + print -e "Mismatching rust compiler versions specified in `Cargo.toml` and `rust-toolchain.toml`" + print -e $"Cargo.toml: ($msrv_spec)" + print -e $"rust-toolchain.toml: ($toolchain_spec)" + exit 1 +} diff --git a/nushell/.github/workflows/ci.yml b/nushell/.github/workflows/ci.yml new file mode 100644 index 0000000..f918a73 --- /dev/null +++ b/nushell/.github/workflows/ci.yml @@ -0,0 +1,212 @@ +on: + pull_request: + push: + branches: + - main + - 'patch-release-*' + +name: continuous-integration + +env: + NUSHELL_CARGO_PROFILE: ci + NU_LOG_LEVEL: DEBUG + # If changing these settings also change toolkit.nu + CLIPPY_OPTIONS: "-D warnings -D clippy::unwrap_used -D clippy::unchecked_duration_subtraction" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + fmt-clippy: + strategy: + fail-fast: true + matrix: + # Pinning to Ubuntu 22.04 because building on newer Ubuntu versions causes linux-gnu + # 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) + # + # Using macOS 13 runner because 14 is based on the M1 and has half as much RAM (7 GB, + # instead of 14 GB) which is too little for us right now. Revisit when `dfr` commands are + # removed and we're only building the `polars` plugin instead + platform: [windows-latest, macos-13, ubuntu-22.04] + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4.1.7 + + - name: Setup Rust toolchain and cache + uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 + + - name: cargo fmt + run: cargo fmt --all -- --check + + # If changing these settings also change toolkit.nu + - name: Clippy + run: cargo clippy --workspace --exclude nu_plugin_* -- $CLIPPY_OPTIONS + + # In tests we don't have to deny unwrap + - name: Clippy of tests + run: cargo clippy --tests --workspace --exclude nu_plugin_* -- -D warnings + + - name: Clippy of benchmarks + run: cargo clippy --benches --workspace --exclude nu_plugin_* -- -D warnings + + tests: + strategy: + fail-fast: true + matrix: + platform: [windows-latest, macos-latest, ubuntu-22.04] + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4.1.7 + + - name: Setup Rust toolchain and cache + uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 + + - name: Tests + run: cargo 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 + + std-lib-and-python-virtualenv: + strategy: + fail-fast: true + matrix: + platform: [ubuntu-22.04, macos-latest, windows-latest] + py: + - py + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4.1.7 + + - name: Setup Rust toolchain and cache + uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 + + - name: Install Nushell + run: cargo install --path . --locked --force + + - name: Standard library tests + run: nu -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std' + + - name: Ensure that Cargo.toml MSRV and rust-toolchain.toml use the same version + run: nu .github/workflows/check-msrv.nu + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install virtualenv + run: pip install virtualenv + shell: bash + + - name: Test Nushell in virtualenv + run: nu scripts/test_virtualenv.nu + shell: bash + + - 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 + + plugins: + strategy: + fail-fast: true + matrix: + # Using macOS 13 runner because 14 is based on the M1 and has half as much RAM (7 GB, + # 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 }} + + steps: + - uses: actions/checkout@v4.1.7 + + - name: Setup Rust toolchain and cache + uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 + + - name: Clippy + run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS + + - name: Tests + run: cargo test --profile ci --package 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 + + 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.12.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 }} diff --git a/nushell/.github/workflows/friendly-config-reminder.yml b/nushell/.github/workflows/friendly-config-reminder.yml new file mode 100644 index 0000000..a45e9d2 --- /dev/null +++ b/nushell/.github/workflows/friendly-config-reminder.yml @@ -0,0 +1,25 @@ +name: Comment on changes to the config +on: + pull_request_target: + paths: + - 'crates/nu-protocol/src/config/**' +jobs: + comment: + runs-on: ubuntu-latest + steps: + - name: Check if there is already a bot comment + uses: peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: Hey, just a bot checking in! + - name: Create comment if there is not + if: steps.fc.outputs.comment-id == '' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + Hey, just a bot checking in! You edited files related to the configuration. + If you changed any of the default values or added a new config option, don't forget to update the [`doc_config.nu`](https://github.com/nushell/nushell/blob/main/crates/nu-utils/src/default_files/doc_config.nu) which documents the options for our users including the defaults provided by the Rust implementation. + If you didn't make a change here, you can just ignore me. diff --git a/nushell/.github/workflows/labels.yml b/nushell/.github/workflows/labels.yml new file mode 100644 index 0000000..96b84c6 --- /dev/null +++ b/nushell/.github/workflows/labels.yml @@ -0,0 +1,19 @@ +# Automatically labels PRs based on the configuration file +# you are probably looking for 👉 `.github/labeler.yml` +name: Label PRs + +on: + - pull_request_target + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + if: github.repository_owner == 'nushell' + steps: + - uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: true \ No newline at end of file diff --git a/nushell/.github/workflows/milestone.yml b/nushell/.github/workflows/milestone.yml new file mode 100644 index 0000000..18eef42 --- /dev/null +++ b/nushell/.github/workflows/milestone.yml @@ -0,0 +1,30 @@ +# Description: +# - Add milestone to a merged PR automatically +# - Add milestone to a closed issue that has a merged PR fix (if any) + +name: Milestone Action +on: + issues: + types: [closed] + pull_request_target: + types: [closed] + +jobs: + update-milestone: + runs-on: ubuntu-latest + name: Milestone Update + steps: + - name: Set Milestone for PR + uses: hustcer/milestone-action@main + if: github.event.pull_request.merged == true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Bind milestone to closed issue that has a merged PR fix + - name: Set Milestone for Issue + uses: hustcer/milestone-action@v2 + if: github.event.issue.state == 'closed' + with: + action: bind-issue + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/nushell/.github/workflows/nightly-build.yml b/nushell/.github/workflows/nightly-build.yml new file mode 100644 index 0000000..5fdf3ef --- /dev/null +++ b/nushell/.github/workflows/nightly-build.yml @@ -0,0 +1,284 @@ +# +# REF: +# 1. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude +# 2. https://github.com/JasonEtco/create-an-issue +# 3. https://docs.github.com/en/actions/learn-github-actions/variables +# 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 + +on: + push: + branches: + - nightly # Just for test purpose only with the nightly repo + # This schedule will run only from the default branch + schedule: + - cron: '15 0 * * *' # run at 00:15 AM UTC + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + prepare: + name: Prepare + 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 + # 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: + - name: Checkout + uses: actions/checkout@v4 + if: github.repository == 'nushell/nightly' + with: + ref: main + fetch-depth: 0 + # Configure PAT here: https://github.com/settings/tokens for the push operation in the following steps + token: ${{ secrets.WORKFLOW_TOKEN }} + + - name: Setup Nushell + uses: hustcer/setup-nu@v3 + if: github.repository == 'nushell/nightly' + with: + version: 0.105.1 + + # Synchronize the main branch of nightly repo with the main branch of Nushell official repo + - name: Prepare for Nightly Release + shell: nu {0} + if: github.repository == 'nushell/nightly' + run: | + cd $env.GITHUB_WORKSPACE + git checkout main + # We can't push if no user name and email are configured + git config user.name 'hustcer' + git config user.email 'hustcer@outlook.com' + git pull origin main + git remote add src https://github.com/nushell/nushell.git + git fetch src main + # All the changes will be overwritten by the upstream main branch + git reset --hard src/main + git push origin main -f + + - name: Create Tag and Output Tag Name + if: github.repository == 'nushell/nightly' + id: vars + shell: nu {0} + run: | + let date = date now | format date %m%d + let version = open Cargo.toml | get package.version + let sha_short = (git rev-parse --short origin/main | str trim | str substring 0..6) + let latest_meta = http get https://api.github.com/repos/nushell/nightly/releases + | sort-by -r created_at + | where tag_name =~ nightly + | get tag_name?.0? | default '' + | parse '{version}-nightly.{build}+{hash}' + if ($latest_meta.0?.hash? | default '') == $sha_short { + print $'(ansi g)Latest nightly build is up-to-date, skip rebuilding.(ansi reset)' + $'skip=true(char nl)' o>> $env.GITHUB_OUTPUT + exit 0 + } + let prev_ver = $latest_meta.0?.version? | default '0.0.0' + let build = if ($latest_meta | is-empty) or ($version != $prev_ver) { 1 } else { + ($latest_meta | get build?.0? | default 0 | into int) + 1 + } + let nightly_tag = $'($version)-nightly.($build)+($sha_short)' + $'build_date=($date)(char nl)' o>> $env.GITHUB_OUTPUT + $'nightly_tag=($nightly_tag)(char nl)' o>> $env.GITHUB_OUTPUT + if (git ls-remote --tags origin $nightly_tag | is-empty) { + ls **/Cargo.toml | each {|file| + open --raw $file.name + | str replace --all $'version = "($version)"' $'version = "($version)-nightly.($build)"' + | save --force $file.name + } + # Disable the following two workflows for the automatic committed changes + rm .github/workflows/ci.yml + rm .github/workflows/audit.yml + + git add . + git commit -m $'Update version to ($version)-nightly.($build)' + git tag -a $nightly_tag -m $'Nightly build from ($sha_short)' + git push origin --tags + git push origin main -f + } + + release: + name: Nu + needs: prepare + if: needs.prepare.outputs.skip != 'true' + 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 + - aarch64-unknown-linux-musl + - armv7-unknown-linux-gnueabihf + - armv7-unknown-linux-musleabihf + - riscv64gc-unknown-linux-gnu + - loongarch64-unknown-linux-gnu + - loongarch64-unknown-linux-musl + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: aarch64-pc-windows-msvc + os: windows-11-arm + - target: x86_64-unknown-linux-gnu + os: ubuntu-22.04 + - target: x86_64-unknown-linux-musl + os: ubuntu-22.04 + - target: aarch64-unknown-linux-gnu + os: ubuntu-22.04 + - target: aarch64-unknown-linux-musl + os: ubuntu-22.04 + - target: armv7-unknown-linux-gnueabihf + os: ubuntu-22.04 + - target: armv7-unknown-linux-musleabihf + os: ubuntu-22.04 + - target: riscv64gc-unknown-linux-gnu + os: ubuntu-22.04 + - target: loongarch64-unknown-linux-gnu + os: ubuntu-22.04 + - target: loongarch64-unknown-linux-musl + os: ubuntu-22.04 + + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v4 + with: + ref: main + 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 + run: | + echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml + + - name: Setup Rust toolchain and cache + uses: actions-rust-lang/setup-rust-toolchain@v1 + # 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 + with: + version: 0.105.1 + + - name: Release Nu Binary + id: nu + run: nu .github/workflows/release-pkg.nu + env: + OS: ${{ matrix.os }} + REF: ${{ github.ref }} + TARGET: ${{ matrix.target }} + + - name: Create an Issue for Release Failure + if: ${{ failure() }} + uses: JasonEtco/create-an-issue@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + update_existing: true + search_existing: open + filename: .github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md + + # 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.9 + if: ${{ startsWith(github.repository, 'nushell/nightly') }} + with: + prerelease: true + files: | + ${{ steps.nu.outputs.msi }} + ${{ steps.nu.outputs.archive }} + tag_name: ${{ needs.prepare.outputs.nightly_tag }} + name: ${{ needs.prepare.outputs.build_date }}-${{ needs.prepare.outputs.nightly_tag }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + sha256sum: + needs: [prepare, release] + name: Create Sha256sum + runs-on: ubuntu-latest + if: github.repository == 'nushell/nightly' + steps: + - name: Download Release Archives + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + gh release download ${{ needs.prepare.outputs.nightly_tag }} + --repo ${{ github.repository }} + --pattern '*' + --dir release + - name: Create Checksums + run: cd release && shasum -a 256 * > ../SHA256SUMS + - name: Publish Checksums + uses: softprops/action-gh-release@v2.0.9 + with: + draft: false + prerelease: true + files: SHA256SUMS + tag_name: ${{ needs.prepare.outputs.nightly_tag }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + cleanup: + name: Cleanup + # Should only run in nushell/nightly repo + if: github.repository == 'nushell/nightly' + needs: [release, sha256sum] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - name: Setup Nushell + uses: hustcer/setup-nu@v3 + with: + version: 0.105.1 + + # Keep the last a few releases + - name: Delete Older Releases + shell: nu {0} + run: | + 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..) + for release in $deprecated { + print $'Deleting tag ($release.tag_name)' + git push origin --delete $release.tag_name + print $'Deleting release ($release.tag_name)' + let delete_url = $'https://api.github.com/repos/nushell/nightly/releases/($release.id)' + let version = "X-GitHub-Api-Version: 2022-11-28" + let accept = "Accept: application/vnd.github+json" + let auth = "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" + # http delete $delete_url -H $version -H $auth -H $accept + curl -L -X DELETE -H $accept -H $auth -H $version $delete_url + } diff --git a/nushell/.github/workflows/release-msi.nu b/nushell/.github/workflows/release-msi.nu new file mode 100755 index 0000000..df7bbb7 --- /dev/null +++ b/nushell/.github/workflows/release-msi.nu @@ -0,0 +1,62 @@ +#!/usr/bin/env nu + +# Created: 2025/05/21 19:05:20 +# Description: +# A script to build Windows MSI packages for NuShell. Need wix 6.0 to be installed. +# The script will download the specified NuShell release, extract it, and create an MSI package. +# Can be run locally or in GitHub Actions. +# To run this script locally: +# load-env { TARGET: 'x86_64-pc-windows-msvc' REF: '0.103.0' GITHUB_REPOSITORY: 'nushell/nushell' } +# nu .github/workflows/release-msi.nu + +def build-msi [] { + let target = $env.TARGET + # We should read the version from the environment variable first + # As we may build the MSI package for a specific version not the latest one + let version = $env.MSI_VERSION? | default (open Cargo.toml | get package.version) + let arch = if $nu.os-info.arch =~ 'x86_64' { 'x64' } else { 'arm64' } + + print $'Building msi package for (ansi g)($target)(ansi reset) with version (ansi g)($version)(ansi reset) from tag (ansi g)($env.REF)(ansi reset)...' + fetch-nu-pkg + # Create extra Windows msi release package if dotnet and wix are available + let installed = [dotnet wix] | all { (which $in | length) > 0 } + if $installed and (wix --version | split row . | first | into int) >= 6 { + + print $'(char nl)Start creating Windows msi package with the following contents...' + cd wix; hr-line + cp nu/README.txt . + ls -f nu/* | print + ./nu/nu.exe -c $'NU_RELEASE_VERSION=($version) dotnet build -c Release -p:Platform=($arch)' + glob **/*.msi | print + # Workaround for https://github.com/softprops/action-gh-release/issues/280 + let wixRelease = (glob **/*.msi | where $it =~ bin | get 0 | str replace --all '\' '/') + let msi = $'($wixRelease | path dirname)/nu-($version)-($target).msi' + mv $wixRelease $msi + print $'MSI archive: ---> ($msi)'; + # Run only in GitHub Actions + if ($env.GITHUB_ACTIONS? | default false | into bool) { + echo $"msi=($msi)(char nl)" o>> $env.GITHUB_OUTPUT + } + } +} + +def fetch-nu-pkg [] { + mkdir wix/nu + # See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + gh release download $env.REF --repo $env.GITHUB_REPOSITORY --pattern $'*-($env.TARGET).zip' --dir wix/nu + cd wix/nu + let pkg = ls *.zip | get name.0 + unzip $pkg + rm $pkg + ls | print +} + +# Print a horizontal line marker +def 'hr-line' [ + --blank-line(-b) +] { + print $'(ansi g)---------------------------------------------------------------------------->(ansi reset)' + if $blank_line { char nl } +} + +alias main = build-msi diff --git a/nushell/.github/workflows/release-msi.yml b/nushell/.github/workflows/release-msi.yml new file mode 100644 index 0000000..6a00f35 --- /dev/null +++ b/nushell/.github/workflows/release-msi.yml @@ -0,0 +1,103 @@ +# +# REF: +# 1. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude +# +name: Build Windows MSI + +on: + workflow_dispatch: + inputs: + tag: + required: true + description: 'Tag to Rebuild MSI' + version: + description: 'Version of Rebuild MSI' + +permissions: + contents: write + packages: write + +defaults: + run: + shell: bash + +jobs: + release: + name: Nu + + strategy: + fail-fast: false + matrix: + target: + - x86_64-pc-windows-msvc + - aarch64-pc-windows-msvc + extra: ['bin'] + + include: + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: aarch64-pc-windows-msvc + os: windows-11-arm + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Wix Toolset 6 for Windows + shell: pwsh + if: ${{ startsWith(matrix.os, 'windows') }} + run: | + dotnet tool install --global wix --version 6.0.0 + dotnet workload install wix + $wixPath = "$env:USERPROFILE\.dotnet\tools" + echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$wixPath;$env:PATH" + wix --version + + - name: Setup Nushell + uses: hustcer/setup-nu@v3 + with: + version: 0.105.1 + + - name: Release MSI Packages + id: nu + run: nu .github/workflows/release-msi.nu + env: + OS: ${{ matrix.os }} + REF: ${{ inputs.tag }} + TARGET: ${{ matrix.target }} + MSI_VERSION: ${{ inputs.version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # REF: https://github.com/marketplace/actions/gh-release + - name: Publish Archive + uses: softprops/action-gh-release@v2.0.5 + with: + tag_name: ${{ inputs.tag }} + files: ${{ steps.nu.outputs.msi }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + sha256sum: + needs: release + name: Create Sha256sum + runs-on: ubuntu-latest + steps: + - name: Download Release Archives + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + gh release download ${{ inputs.tag }} + --repo ${{ github.repository }} + --pattern '*' + --dir release + - name: Create Checksums + run: cd release && rm -f SHA256SUMS && shasum -a 256 * > ../SHA256SUMS + - name: Publish Checksums + uses: softprops/action-gh-release@v2.0.5 + with: + files: SHA256SUMS + tag_name: ${{ inputs.tag }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/nushell/.github/workflows/release-pkg.nu b/nushell/.github/workflows/release-pkg.nu new file mode 100755 index 0000000..0d78aff --- /dev/null +++ b/nushell/.github/workflows/release-pkg.nu @@ -0,0 +1,254 @@ +#!/usr/bin/env nu + +# Created: 2022/05/26 19:05:20 +# Description: +# A script to do the github release task, need nushell to be installed. +# REF: +# 1. https://github.com/volks73/cargo-wix + +# Instructions for manually creating an MSI for Winget Releases when they fail +# Added 2022-11-29 when Windows packaging wouldn't work +# Updated again on 2023-02-23 because MSIs are still failing validation +# To run this manual for windows here are the steps I take +# checkout the release you want to publish +# 1. git checkout 0.103.0 +# unset CARGO_TARGET_DIR if set (I have to do this in the parent shell to get it to work) +# 2. $env:CARGO_TARGET_DIR = "" +# 2. hide-env CARGO_TARGET_DIR +# 3. $env.TARGET = 'x86_64-pc-windows-msvc' +# 4. $env.GITHUB_WORKSPACE = 'D:\nushell' +# 5. $env.GITHUB_OUTPUT = 'D:\nushell\output\out.txt' +# 6. $env.OS = 'windows-latest' +# 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') +# make sure aria2c.exe is in your path https://github.com/aria2/aria2 +# 8. $env.Path = ($env.Path | append 'c:\path\to\aria2c') +# make sure you have the wix 6.0 installed: dotnet tool install --global wix --version 6.0.0 +# then build nu*.exe and the MSI installer by running: +# 9. source .github\workflows\release-pkg.nu +# 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 +# on the winget-pkgs PR. To generate the hash, run this command +# 10. open wix\bin\x64\Release\nu-0.103.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 + + +# The main binary file to be released +let bin = 'nu' +let os = $env.OS +let target = $env.TARGET +# Repo source dir like `/home/runner/work/nushell/nushell` +let src = $env.GITHUB_WORKSPACE +let dist = $'($env.GITHUB_WORKSPACE)/output' +let version = (open Cargo.toml | get package.version) + +print $'Debugging info:' +print { version: $version, bin: $bin, os: $os, target: $target, src: $src, dist: $dist }; hr-line -b + +# $env + +let USE_UBUNTU = $os starts-with ubuntu + +print $'(char nl)Packaging ($bin) v($version) for ($target) in ($src)...'; hr-line -b +if not ('Cargo.lock' | path exists) { cargo generate-lockfile } + +print $'Start building ($bin)...'; hr-line + +# ---------------------------------------------------------------------------- +# Build for Ubuntu and macOS +# ---------------------------------------------------------------------------- +if $os in ['macos-latest'] or $USE_UBUNTU { + if $USE_UBUNTU { + sudo apt update + sudo apt-get install libxcb-composite0-dev -y + } + match $target { + 'aarch64-unknown-linux-gnu' => { + sudo apt-get install gcc-aarch64-linux-gnu -y + $env.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = 'aarch64-linux-gnu-gcc' + cargo-build-nu + } + 'riscv64gc-unknown-linux-gnu' => { + sudo apt-get install gcc-riscv64-linux-gnu -y + $env.CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER = 'riscv64-linux-gnu-gcc' + cargo-build-nu + } + 'armv7-unknown-linux-gnueabihf' => { + sudo apt-get install pkg-config gcc-arm-linux-gnueabihf -y + $env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER = 'arm-linux-gnueabihf-gcc' + cargo-build-nu + } + 'aarch64-unknown-linux-musl' => { + aria2c https://github.com/nushell/integrations/releases/download/build-tools/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://github.com/nushell/integrations/releases/download/build-tools/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 + } + 'loongarch64-unknown-linux-musl' => { + print $"(ansi g)Downloading LoongArch64 musl cross-compilation toolchain...(ansi reset)" + aria2c -q https://github.com/LoongsonLab/oscomp-toolchains-for-oskernel/releases/download/loongarch64-linux-musl-cross-gcc-13.2.0/loongarch64-linux-musl-cross.tgz + tar -xf loongarch64-linux-musl-cross.tgz + $env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.PWD)/loongarch64-linux-musl-cross/bin') + $env.CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_MUSL_LINKER = "loongarch64-linux-musl-gcc" + cargo-build-nu + } + _ => { + # musl-tools to fix 'Failed to find tool. Is `musl-gcc` installed?' + # Actually just for x86_64-unknown-linux-musl target + if $USE_UBUNTU { sudo apt install musl-tools -y } + cargo-build-nu + } + } +} + +# ---------------------------------------------------------------------------- +# Build for Windows without static-link-openssl feature +# ---------------------------------------------------------------------------- +if $os =~ 'windows' { + cargo-build-nu +} + +# ---------------------------------------------------------------------------- +# Prepare for the release archive +# ---------------------------------------------------------------------------- +let suffix = if $os =~ 'windows' { '.exe' } +# nu, nu_plugin_* were all included +let executable = $'target/($target)/release/($bin)*($suffix)' +print $'Current executable file: ($executable)' + +cd $src; mkdir $dist; +rm -rf ...(glob $'target/($target)/release/*.d') ...(glob $'target/($target)/release/nu_pretty_hex*') +print $'(char nl)All executable files:'; hr-line +# We have to use `print` here to make sure the command output is displayed +print (ls -f ($executable | into glob)); sleep 1sec + +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. +Then you can use `plugin use` to load the plugin into your session. +For example: + +> plugin add ./nu_plugin_query +> 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 + +print $'(char nl)Check binary release version detail:'; hr-line +let ver = if $os =~ 'windows' { + (do -i { .\output\nu.exe -c 'version' }) | default '' | str join +} else { + (do -i { ./output/nu -c 'version' }) | default '' | str join +} +if ($ver | str trim | is-empty) { + print $'(ansi r)Incompatible Nu binary: The binary cross compiled is not runnable on current arch...(ansi reset)' +} else { print $ver } + +# ---------------------------------------------------------------------------- +# Create a release archive and send it to output for the following steps +# ---------------------------------------------------------------------------- +cd $dist; print $'(char nl)Creating release archive...'; hr-line +if $os in ['macos-latest'] or $USE_UBUNTU { + + let files = (ls | get name) + let dest = $'($bin)-($version)-($target)' + let archive = $'($dist)/($dest).tar.gz' + + mkdir $dest + $files | each {|it| cp -v $it $dest } + + print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls $dest | print + + tar -czf $archive $dest + print $'archive: ---> ($archive)'; ls $archive + # 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 + +} else if $os =~ 'windows' { + + let releaseStem = $'($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 + 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)(char nl)" o>> $env.GITHUB_OUTPUT + } + + # Create extra Windows msi release package if dotnet and wix are available + let installed = [dotnet wix] | all { (which $in | length) > 0 } + if $installed and (wix --version | split row . | first | into int) >= 6 { + + print $'(char nl)Start creating Windows msi package with the following contents...' + cd $src; cd wix; hr-line; mkdir nu + # Wix need the binaries be stored in nu folder + cp -r ($'($dist)/*' | into glob) nu/ + cp $'($dist)/README.txt' . + ls -f nu/* | print + ./nu/nu.exe -c $'NU_RELEASE_VERSION=($version) dotnet build -c Release -p:Platform=($arch)' + glob **/*.msi | print + # Workaround for https://github.com/softprops/action-gh-release/issues/280 + let wixRelease = (glob **/*.msi | where $it =~ bin | get 0 | str replace --all '\' '/') + let msi = $'($wixRelease | path dirname)/nu-($version)-($target).msi' + mv $wixRelease $msi + print $'MSI archive: ---> ($msi)'; + echo $"msi=($msi)(char nl)" o>> $env.GITHUB_OUTPUT + } +} + +def fetch-less [ + arch: string = 'x64' # The architecture to fetch +] { + let less_zip = $'less-($arch).zip' + print $'Fetching less archive: (ansi g)($less_zip)(ansi reset)' + let url = $'https://github.com/jftuga/less-Windows/releases/download/less-v668/($less_zip)' + http get https://github.com/jftuga/less-Windows/blob/master/LICENSE | save -rf LICENSE-for-less.txt + http get $url | save -rf $less_zip + unzip $less_zip + rm $less_zip lesskey.exe +} + +def 'cargo-build-nu' [] { + if $os =~ 'windows' { + cargo build --release --all --target $target + } else { + cargo build --release --all --target $target --features=static-link-openssl + } +} + +# Print a horizontal line marker +def 'hr-line' [ + --blank-line(-b) +] { + print $'(ansi g)---------------------------------------------------------------------------->(ansi reset)' + if $blank_line { char nl } +} + +# Get the specified env key's value or '' +def 'get-env' [ + key: string # The key to get it's env value + default: string = '' # The default value for an empty env +] { + $env | get -i $key | default $default +} diff --git a/nushell/.github/workflows/release.yml b/nushell/.github/workflows/release.yml new file mode 100644 index 0000000..728b590 --- /dev/null +++ b/nushell/.github/workflows/release.yml @@ -0,0 +1,141 @@ +# +# REF: +# 1. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude +# +name: Create Release Draft + +on: + workflow_dispatch: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + - '!*nightly*' # Don't trigger release for nightly tags + +defaults: + run: + shell: bash + +jobs: + release: + name: Nu + + 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 + - aarch64-unknown-linux-musl + - armv7-unknown-linux-gnueabihf + - armv7-unknown-linux-musleabihf + - riscv64gc-unknown-linux-gnu + - loongarch64-unknown-linux-gnu + - loongarch64-unknown-linux-musl + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: aarch64-pc-windows-msvc + os: windows-11-arm + - target: x86_64-unknown-linux-gnu + os: ubuntu-22.04 + - target: x86_64-unknown-linux-musl + os: ubuntu-22.04 + - target: aarch64-unknown-linux-gnu + os: ubuntu-22.04 + - target: aarch64-unknown-linux-musl + os: ubuntu-22.04 + - target: armv7-unknown-linux-gnueabihf + os: ubuntu-22.04 + - target: armv7-unknown-linux-musleabihf + os: ubuntu-22.04 + - target: riscv64gc-unknown-linux-gnu + os: ubuntu-22.04 + - target: loongarch64-unknown-linux-gnu + os: ubuntu-22.04 + - target: loongarch64-unknown-linux-musl + os: ubuntu-22.04 + + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v4 + + - name: Install Wix Toolset 6 for Windows + shell: pwsh + if: ${{ startsWith(matrix.os, 'windows') }} + run: | + dotnet tool install --global wix --version 6.0.0 + dotnet workload install wix + $wixPath = "$env:USERPROFILE\.dotnet\tools" + echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$wixPath;$env:PATH" + wix --version + + - name: Update Rust Toolchain Target + run: | + echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml + + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1.12.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 + with: + version: 0.105.1 + + - name: Release Nu Binary + id: nu + run: nu .github/workflows/release-pkg.nu + env: + OS: ${{ matrix.os }} + REF: ${{ github.ref }} + TARGET: ${{ matrix.target }} + + # WARN: Don't upgrade this action due to the release per asset issue. + # See: https://github.com/softprops/action-gh-release/issues/445 + - name: Publish Archive + uses: softprops/action-gh-release@v2.0.5 + if: ${{ startsWith(github.ref, 'refs/tags/') }} + with: + draft: true + files: | + ${{ steps.nu.outputs.msi }} + ${{ steps.nu.outputs.archive }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + sha256sum: + needs: release + name: Create Sha256sum + runs-on: ubuntu-latest + steps: + - name: Download Release Archives + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + gh release download ${{ github.ref_name }} + --repo ${{ github.repository }} + --pattern '*' + --dir release + - name: Create Checksums + run: cd release && shasum -a 256 * > ../SHA256SUMS + - name: Publish Checksums + uses: softprops/action-gh-release@v2.0.5 + with: + draft: true + files: SHA256SUMS + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/nushell/.github/workflows/typos.yml b/nushell/.github/workflows/typos.yml new file mode 100644 index 0000000..8a87fa3 --- /dev/null +++ b/nushell/.github/workflows/typos.yml @@ -0,0 +1,13 @@ +name: Typos +on: [pull_request] + +jobs: + run: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4.1.7 + + - name: Check spelling + uses: crate-ci/typos@v1.33.1 diff --git a/nushell/.github/workflows/winget-submission.yml b/nushell/.github/workflows/winget-submission.yml new file mode 100644 index 0000000..01981fd --- /dev/null +++ b/nushell/.github/workflows/winget-submission.yml @@ -0,0 +1,34 @@ +name: Submit Nushell package to Windows Package Manager Community Repository + +on: + release: + types: [released] + workflow_dispatch: + inputs: + tag_name: + description: 'Specific tag name' + required: true + type: string + +permissions: + contents: write + packages: write + pull-requests: write + +jobs: + + winget: + name: Publish winget package + runs-on: ubuntu-latest + steps: + - name: Submit package to Windows Package Manager Community Repository + uses: vedantmgoyal2009/winget-releaser@v2 + with: + identifier: Nushell.Nushell + # Exclude all `*-msvc-full.msi` full release files, + # and only the default `*msvc.msi` files will be included + installers-regex: 'msvc\.msi$' + version: ${{ inputs.tag_name || github.event.release.tag_name }} + release-tag: ${{ inputs.tag_name || github.event.release.tag_name }} + token: ${{ secrets.NUSHELL_PAT }} + fork-user: nushell diff --git a/nushell/.gitignore b/nushell/.gitignore new file mode 100644 index 0000000..9a080ea --- /dev/null +++ b/nushell/.gitignore @@ -0,0 +1,55 @@ +/target +/scratch +**/*.rs.bk +history.txt +tests/fixtures/nuplayground +crates/*/target +.mailmap + +# Debian/Ubuntu +debian/.debhelper/ +debian/debhelper-build-stamp +debian/files +debian/nu.substvars +debian/nu/ + +# macOS junk +.DS_Store + +# JetBrains' IDE items +.idea/* + +# VSCode's IDE items +.vscode/* + +# JetBrains' Fleet IDE +.fleet/* + +# Visual Studio Extension SourceGear Rust items +VSWorkspaceSettings.json +unstable_cargo_features.txt + +# Helix configuration folder +.helix/* +.helix +wix/bin/ +wix/obj/ +wix/nu/ + +# Coverage tools +lcov.info +tarpaulin-report.html + +# Visual Studio +.vs/* +*.rsproj +*.rsproj.user +*.sln +*.code-workspace + +# direnv +.direnv/ +.envrc + +# pre-commit-hooks +.pre-commit-config.yaml diff --git a/nushell/CITATION.cff b/nushell/CITATION.cff new file mode 100644 index 0000000..25731e8 --- /dev/null +++ b/nushell/CITATION.cff @@ -0,0 +1,26 @@ +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 diff --git a/nushell/CODE_OF_CONDUCT.md b/nushell/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9f5a648 --- /dev/null +++ b/nushell/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at wycats@gmail.com via email or by reaching out to @jturner, @gedge, or @andras_io on discord. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see + diff --git a/nushell/CONTRIBUTING.md b/nushell/CONTRIBUTING.md new file mode 100644 index 0000000..a04d374 --- /dev/null +++ b/nushell/CONTRIBUTING.md @@ -0,0 +1,236 @@ +# Contributing + +Welcome to Nushell and thank you for considering contributing! + +## Table of contents +- [Proposing design changes](#proposing-design-changes) +- [Developing](#developing) + - [Setup](#setup) + - [Tests](#tests) + - [Useful commands](#useful-commands) + - [Debugging tips](#debugging-tips) +- [Git etiquette](#git-etiquette) +- [License](#license) + +## Other helpful resources + +More resources can be found in the nascent [developer documentation](devdocs/README.md) in this repo. + +- [Developer FAQ](devdocs/FAQ.md) +- [Platform support policy](devdocs/PLATFORM_SUPPORT.md) +- [Our Rust style](devdocs/rust_style.md) + +## Proposing design changes + +First of all, before diving into the code, if you want to create a new feature, change something significantly, and especially if the change is user-facing, it is a good practice to first get an approval from the core team before starting to work on it. +This saves both your and our time if we realize the change needs to go another direction before spending time on it. +So, please, reach out and tell us what you want to do. +This will significantly increase the chance of your PR being accepted. + +The review process can be summarized as follows: +1. You want to make some change to Nushell that is more involved than simple bug-fixing. +2. Go to [Discord](https://discordapp.com/invite/NtAbbGn) or a [GitHub issue](https://github.com/nushell/nushell/issues/new/choose) and chat with some core team members and/or other contributors about it. +3. After getting a green light from the core team, implement the feature, open a pull request (PR) and write a concise but comprehensive description of the change. +4. If your PR includes any user-facing features (such as adding a flag to a command), clearly list them in the PR description. +5. Then, core team members and other regular contributors will review the PR and suggest changes. +6. When we all agree, the PR will be merged. +7. If your PR includes any user-facing features, make sure the changes are also reflected in [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged. +8. Congratulate yourself, you just improved Nushell! :-) + +## Developing + +### Setup + +Nushell requires a recent Rust toolchain and some dependencies; [refer to the Nu Book for up-to-date requirements](https://www.nushell.sh/book/installation.html#build-from-source). After installing dependencies, you should be able to clone+build Nu like any other Rust project: + +```bash +git clone https://github.com/nushell/nushell +cd nushell +cargo build +``` + +### Tests + +It is good practice to cover your changes with a test. Also, try to think about corner cases and various ways how your changes could break. Cover those in the tests as well. + +Tests can be found in different places: +* `/tests` +* command examples +* crate-specific tests + +Most of the tests are built upon the `nu-test-support` crate. For testing specific features, such as running Nushell in a REPL mode, we have so called "testbins". For simple tests, you can find `run_test()` and `fail_test()` functions. + +### Useful Commands + +As Nushell is built using a cargo workspace consisting of multiple crates keep in mind that you may need to pass additional flags compared to how you may be used to it from a single crate project. +Read cargo's documentation for more details: https://doc.rust-lang.org/cargo/reference/workspaces.html + +- Build and run Nushell: + + ```nushell + cargo run + ``` + +- Run Clippy on Nushell: + + ```nushell + cargo clippy --workspace -- -D warnings -D clippy::unwrap_used + ``` + or via the `toolkit.nu` command: + ```nushell + use toolkit.nu clippy + clippy + ``` + +- Run all tests: + + ```nushell + cargo test --workspace + ``` + + or via the `toolkit.nu` command: + ```nushell + use toolkit.nu test + test + ``` + +- Run all tests for a specific command + + ```nushell + cargo test --package nu-cli --test main -- commands:: + ``` + +- Check to see if there are code formatting issues + + ```nushell + cargo fmt --all -- --check + ``` + or via the `toolkit.nu` command: + ```nushell + use toolkit.nu fmt + fmt --check + ``` + +- Format the code in the project + + ```nushell + cargo fmt --all + ``` + or via the `toolkit.nu` command: + ```nushell + use toolkit.nu fmt + fmt + ``` + +- Set up `git` hooks to check formatting and run `clippy` before committing and pushing: + + ```nushell + use toolkit.nu setup-git-hooks + setup-git-hooks + ``` + _Unfortunately, this hook isn't available on Windows._ + +### Debugging Tips + +- To view verbose logs when developing, enable the `trace` log level. + + ```nushell + cargo run --release -- --log-level trace + ``` + +- To redirect trace logs to a file, enable the `--log-target file` switch. + ```nushell + cargo run --release -- --log-level trace --log-target file + open $"($nu.temp-path)/nu-($nu.pid).log" + ``` + +## Git etiquette + +As nushell thrives on its broad base of volunteer contributors and maintainers with different backgrounds we have a few guidelines for how we best utilize git and GitHub for our contributions. We strive to balance three goals with those recommendations: + +1. The **volunteer maintainers and contributors** can easily follow the changes you propose, gauge the impact, and come to help you or make a decision. +2. **You as a contributor** can focus most of your time on improving the quality of the nushell project and contributing your expertise to the code or documentation. +3. Making sure we can trace back *why* decisions were made in the past. +This includes discarded approaches. Also we want to quickly identify regressions and fix when something broke. + +### How we merge PRs + +In general the maintainers **squash** all changes of your PR into a single commit when merging. + +This keeps a clean enough linear history, while not forcing you to conform to a too strict style while iterating in your PR or fixing small problems. As an added benefit the commits on the `main` branch are tied to the discussion that happened in the PR through their `#1234` issue number. + +> **Note** +> **Pro advice:** In some circumstances, we can agree on rebase-merging a particularly large but connected PR as a series of atomic commits onto the `main` branch to ensure we can more easily revert or bisect particular aspects. + +### A good PR makes a change! + +As a result of this PR-centric strategy and the general goal that the reviewers should easily understand your change, the **PR title and description matters** a great deal! + +Make sure your description is **concise** but contains all relevant information and context. +This means demonstrating what changes, ideally through nushell code or output **examples**. +Furthermore links to technical documentation or instructions for folks that want to play with your change make the review process much easier. + +> **Note** +> Try to follow the suggestions in our PR message template to make sure we can quickly focus on the technical merits and impact on the users. + +#### A PR should limit itself to a single functional change or related set of same changes. + +Mixing different changes in the same PR will make the review process much harder. A PR might get stuck on one aspect while we would actually like to land another change. Furthermore, if we are forced to revert a change, mixing and matching different aspects makes fixing bugs or regressions much harder. + +Thus, please try to **separate out unrelated changes**! +**Don't** mix unrelated refactors with a potentially contested change. +Stylistic fixes and housekeeping can be bundled up into singular PRs. + +#### Guidelines for the PR title + +The PR title should be concise but contain everything for a contributor to know if they should help out in the review of this particular change. + +**DON'T** +- `Update file/in/some/deeply/nested/path.rs` + - Why are you making this change? +- `Fix 2134` + - What has to be fixed? + - Hard to follow when not online on GitHub. +- ``Ignore `~` expansion`` + - In what context should this change take effect? +- `[feature] refactor the whole parser and also make nushell indentation-sensitive, upgrade to using Cpython. Let me know what you think!` + - Be concise + - Maybe break up into smaller commits or PRs if the title already appears too long? + +**DO** +- Mention the nushell feature or command that is affected. + - ``Fix URL parsing in `http get` (issue #1234)`` +- You can mention the issue number if other context is there. + - In general, mention all related issues in the description to crosslink (e.g. `Fixes #1234`, `Closes #6789`) +- For internal changes mention the area or symbols affected if it helps to clarify + - ``Factor out `quote_string()` from parser to reuse in `explore` `` + +### Review process / Merge conflicts + +> **Note** +> Keep in mind that the maintainers are volunteers that need to allocate their attention to several different areas and active PRs. We will try to get back to you as soon as possible. + +You can help us to make the review process a smooth experience: +- Testing: + - We generally review in detail after all the tests pass. Let us know if there is a problem you want to discuss to fix a test failure or forces us to accept a breaking change. + - If you fix a bug, it is highly recommended that you add a test that reproduces the original issue/panic in a minimal form. + - In general, added tests help us to understand which assumptions go into a particular addition/change. + - Try to also test corner cases where those assumptions might break. This can be more valuable than simply adding many similar tests. +- Commit history inside a PR during code review: + - Good **atomic commits** can help follow larger changes, but we are not pedantic. + - We don't shame fixup commits while you try to figure out a problem. They can help others see what you tried and what didn't work. (see our [squash policy](#how-we-merge-prs)) + - During active review constant **force pushing** just to amend changes can be confusing! + - GitHub's UI presents reviewers with less options to compare diffs + - fetched branches for experimentation become invalid! + - the notification a maintainer receives has a low signal-to-noise ratio + - Git pros *can* use their judgement to rebase/squash to clean up the history *if it aids the understanding* of a larger change during review +- Merge conflicts: + - In general you should take care of resolving merge conflicts. + - Use your judgement whether to `git merge main` or to `git rebase main` + - 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. + - 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 + +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. diff --git a/nushell/Cargo.lock b/nushell/Cargo.lock new file mode 100644 index 0000000..72366f8 --- /dev/null +++ b/nushell/Cargo.lock @@ -0,0 +1,8867 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alphanumeric-sort" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d67c60c5f10f11c6ee04de72b2dd98bb9d2548cbc314d22a609bfa8bd9e87e8f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi-str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060de1453b69f46304b28274f382132f4e72c55637cf362920926a70d090890d" +dependencies = [ + "ansitok", +] + +[[package]] +name = "ansitok" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a8acea8c2f1c60f0a92a8cd26bf96ca97db56f10bbcab238bbe0cceba659ee" +dependencies = [ + "nom 7.1.3", + "vte 0.14.1", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arboard" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +dependencies = [ + "clipboard-win", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "argminmax" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f13d10a41ac8d2ec79ee34178d61e6f47a29c2edfe7ef1721c7383b0359e65" +dependencies = [ + "num-traits", +] + +[[package]] +name = "array-init-cursor" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed51fe0f224d1d4ea768be38c51f9f831dee9d05c163c11fba0b8c44387b1fc3" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "atoi_simd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4790f9e8961209112beb783d85449b508673cf4a6a419c8449b210743ac4dbe9" + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "avro-schema" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5281855b39aba9684d2f47bf96983fbfd8f1725f12fabb0513a8ab879647bbd" +dependencies = [ + "crc", + "fallible-streaming-iterator", + "libflate", + "serde", + "serde_json", + "snap", +] + +[[package]] +name = "aws-config" +version = "1.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b49afaa341e8dd8577e1a2200468f98956d6eda50bcf4a53246cc00174ba924" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 0.2.12", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-runtime" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09677244a9da92172c8dc60109b4a9658597d4d298b188dd0018b6a66b410ca4" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fea2f3a8bb3bd10932ae7ad59cc59f65f270fc9183a7e91f501dc5efbef7ee" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ada54e5f26ac246dc79727def52f7f8ed38915cb47781e2a72213957dc3a7d5" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5619742a0d8f253be760bfbb8e8e8368c69e3587e4637af5754e488a611499b1" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.2.0", + "once_cell", + "percent-encoding", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "http-body 1.0.1", + "httparse", + "hyper 0.14.31", + "hyper-rustls 0.24.2", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls 0.21.12", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.2.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "http 0.2.12", + "http 1.2.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.90", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "boxcar" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c4925bc979b677330a8c7fe7a8c94af2dbb4a2d37b4a20a80d884400f46baa" + +[[package]] +name = "bracoxide" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3572b24445a122332bb25a2637248d62ca8b567351d98b1194ca4132c61810bd" + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +dependencies = [ + "serde", +] + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "bytesize" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" + +[[package]] +name = "calamine" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e02a18e79de779a78b0a6ec84a3deed1ff0607dd970a11369f993263f99f1a" +dependencies = [ + "atoi_simd", + "byteorder", + "chrono", + "codepage", + "encoding_rs", + "fast-float2", + "log", + "quick-xml 0.37.1", + "serde", + "zip", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chardetng" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" +dependencies = [ + "cfg-if", + "encoding_rs", + "memchr", +] + +[[package]] +name = "charset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" +dependencies = [ + "base64 0.22.1", + "encoding_rs", +] + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "pure-rust-locales", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "chrono-humanize" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" +dependencies = [ + "chrono", +] + +[[package]] +name = "chrono-tz" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "colorz" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceb37c5798821e37369cb546f430f19da2f585e0364c9615ae340a9f2e6067b" +dependencies = [ + "supports-color", +] + +[[package]] +name = "comfy-table" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f165e7b643266ea80cb858aed492ad9280e3e05ce24d4a99d7d7b889b6a4d9" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width 0.2.0", +] + +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width 0.1.11", + "windows-sys 0.52.0", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio 1.0.3", + "parking_lot", + "rustix 0.38.42", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.90", +] + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix 0.29.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "curl" +version = "0.4.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.78+curl-8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eec768341c5c7789611ae51cf6c459099f22e64a5d5d0ce4892434e33821eaf" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.52.0", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.90", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-new" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "devicons" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830e47e2f330cf4fdd5a958dcef921b9523ffc21ab6713aa5e77ba2cce03904b" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "fuzzy-matcher", + "shell-words", + "thiserror 1.0.69", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtparse" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb403c0926d35af2cc54d961bc2696a10d40725c08360ef69db04a4c201fd7" +dependencies = [ + "chrono", + "lazy_static", + "num-traits", + "rust_decimal", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ego-tree" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "eml-parser" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7db8d15f812e08f76427c555195583e3ab8a99fd8b208f8d9e2899262e80f2e" +dependencies = [ + "regex", + "rfc2047-decoder", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "error-code" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "ethnum" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fast-float2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix 0.38.42", + "windows-sys 0.52.0", +] + +[[package]] +name = "file-id" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc904b9bbefcadbd8e3a9fb0d464a9b979de6324c03b3c663e8994f46a5be36" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "filesize" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d741e2415d4e2e5bd1c1d00409d1a8865a57892c2d689b504365655d237d43" +dependencies = [ + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "git2" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff" +dependencies = [ + "bitflags 2.6.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "gjson" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43503cc176394dd30a6525f5f36e838339b8b5619be33ed9a7783841580a97b6" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "glob-match" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" + +[[package]] +name = "goblin" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27c1b4369c2cd341b5de549380158b105a04c331be5db9110eef7b6d2742134" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.2.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "halfbrown" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" +dependencies = [ + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", + "rayon", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "rayon", + "serde", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "html5ever" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.2.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "human-date-parser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406f83c56de4b2c9183be52ae9a4fefa22c0e0c3d3d7ef80be26eaee11c7110e" +dependencies = [ + "chrono", + "pest", + "pest_consume", + "pest_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.31", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper 1.5.1", + "hyper-util", + "rustls 0.23.20", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.1", + "tower-service", + "webpki-roots 0.26.8", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.5.1", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ical" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "indicatif" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.0", + "web-time", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instability" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "interprocess" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "894148491d817cb36b6f778017b8ac46b17408d522dd90f539d677ea938362eb" +dependencies = [ + "doctest-file", + "libc", + "recvmsg", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "inventory" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_debug" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407" + +[[package]] +name = "is_executable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +dependencies = [ + "winapi", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonpath_lib_polars_vendor" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4bd9354947622f7471ff713eacaabdb683ccb13bba4edccaab9860abf480b7d" +dependencies = [ + "log", + "serde", + "serde_json", +] + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" + +[[package]] +name = "libflate" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +dependencies = [ + "rle-decode-fast", +] + +[[package]] +name = "libgit2-sys" +version = "0.18.0+1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libproc" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78a09b56be5adbcad5aa1197371688dc6bb249a26da3bca2011ee2fb987ebfb" +dependencies = [ + "bindgen", + "errno", + "libc", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +dependencies = [ + "serde", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "lscolors" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61183da5de8ba09a58e330d55e5ea796539d8443bd00fdeb863eac39724aa4ab" +dependencies = [ + "aho-corasick", + "nu-ansi-term", +] + +[[package]] +name = "lsp-server" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9" +dependencies = [ + "crossbeam-channel", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "lsp-textdocument" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d564d595f4e3dcd3c071bf472dbd2cac53bc3665ae7222d2abfecd18feaed2c" +dependencies = [ + "lsp-types", + "serde_json", +] + +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "lz4" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d1febb2b4a79ddd1980eede06a8f7902197960aa0383ffcfdd62fe723036725" +dependencies = [ + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever 0.27.0", + "markup5ever 0.12.1", + "tendril", + "xml5ever", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.11", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mockito" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48" +dependencies = [ + "assert-json-diff", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "log", + "rand 0.9.0", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "multipart-rs" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cae00e7e52aa5072342ef9a2ccd71669be913c2176a81a665b1f9cd79345f2" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "memchr", + "mime", + "uuid", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.6.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" +dependencies = [ + "file-id", + "log", + "notify", + "parking_lot", + "walkdir", +] + +[[package]] +name = "now" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89e9874397a1f0a52fc1f197a8effd9735223cb2390e9dcc83ac6cd02923d0" +dependencies = [ + "chrono", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu" +version = "0.105.2" +dependencies = [ + "assert_cmd", + "crossterm", + "ctrlc", + "dirs", + "fancy-regex", + "log", + "miette", + "multipart-rs", + "nix 0.29.0", + "nu-cli", + "nu-cmd-base", + "nu-cmd-extra", + "nu-cmd-lang", + "nu-cmd-plugin", + "nu-command", + "nu-engine", + "nu-explore", + "nu-lsp", + "nu-parser", + "nu-path", + "nu-plugin-core", + "nu-plugin-engine", + "nu-plugin-protocol", + "nu-protocol", + "nu-std", + "nu-system", + "nu-test-support", + "nu-utils", + "openssl", + "pretty_assertions", + "reedline", + "rstest", + "serde_json", + "serial_test", + "simplelog", + "tango-bench", + "tempfile", + "time", + "winresource", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "nu-cli" +version = "0.105.2" +dependencies = [ + "chrono", + "crossterm", + "fancy-regex", + "is_executable", + "log", + "lscolors", + "miette", + "nu-ansi-term", + "nu-cmd-base", + "nu-cmd-lang", + "nu-color-config", + "nu-command", + "nu-engine", + "nu-glob", + "nu-parser", + "nu-path", + "nu-plugin-engine", + "nu-protocol", + "nu-std", + "nu-test-support", + "nu-utils", + "nucleo-matcher", + "percent-encoding", + "reedline", + "rstest", + "strum", + "sysinfo", + "tempfile", + "unicode-segmentation", + "uuid", + "which", +] + +[[package]] +name = "nu-cmd-base" +version = "0.105.2" +dependencies = [ + "indexmap", + "miette", + "nu-engine", + "nu-parser", + "nu-path", + "nu-protocol", +] + +[[package]] +name = "nu-cmd-extra" +version = "0.105.2" +dependencies = [ + "fancy-regex", + "heck", + "itertools 0.14.0", + "mime", + "nu-ansi-term", + "nu-cmd-base", + "nu-cmd-lang", + "nu-command", + "nu-engine", + "nu-json", + "nu-parser", + "nu-pretty-hex", + "nu-protocol", + "nu-test-support", + "nu-utils", + "num-traits", + "rust-embed", + "serde", + "serde_urlencoded", + "v_htmlescape", +] + +[[package]] +name = "nu-cmd-lang" +version = "0.105.2" +dependencies = [ + "itertools 0.14.0", + "miette", + "nu-cmd-base", + "nu-engine", + "nu-parser", + "nu-protocol", + "nu-utils", + "quickcheck", + "quickcheck_macros", + "shadow-rs", +] + +[[package]] +name = "nu-cmd-plugin" +version = "0.105.2" +dependencies = [ + "itertools 0.14.0", + "nu-engine", + "nu-path", + "nu-plugin-engine", + "nu-protocol", +] + +[[package]] +name = "nu-color-config" +version = "0.105.2" +dependencies = [ + "nu-ansi-term", + "nu-engine", + "nu-json", + "nu-protocol", + "nu-test-support", + "serde", +] + +[[package]] +name = "nu-command" +version = "0.105.2" +dependencies = [ + "alphanumeric-sort", + "base64 0.22.1", + "bracoxide", + "brotli", + "byteorder", + "bytesize", + "calamine", + "chardetng", + "chrono", + "chrono-humanize", + "chrono-tz", + "crossterm", + "csv", + "data-encoding", + "devicons", + "dialoguer", + "digest", + "dirs", + "dtparse", + "encoding_rs", + "fancy-regex", + "filesize", + "filetime", + "getrandom 0.2.15", + "human-date-parser", + "indexmap", + "indicatif", + "itertools 0.14.0", + "log", + "lscolors", + "md-5", + "mime", + "mime_guess", + "mockito", + "multipart-rs", + "native-tls", + "nix 0.29.0", + "notify-debouncer-full", + "nu-ansi-term", + "nu-cmd-base", + "nu-cmd-lang", + "nu-color-config", + "nu-engine", + "nu-glob", + "nu-json", + "nu-parser", + "nu-path", + "nu-pretty-hex", + "nu-protocol", + "nu-system", + "nu-table", + "nu-term-grid", + "nu-test-support", + "nu-utils", + "num-format", + "num-traits", + "nuon", + "oem_cp", + "open", + "os_pipe", + "pathdiff", + "percent-encoding", + "pretty_assertions", + "print-positions", + "procfs", + "quick-xml 0.37.1", + "rand 0.9.0", + "rand_chacha 0.9.0", + "rayon", + "reedline", + "rmp", + "roxmltree", + "rstest", + "rstest_reuse", + "rusqlite", + "rustls 0.23.20", + "rustls-native-certs 0.8.1", + "scopeguard", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "sha2", + "sysinfo", + "tabled", + "tempfile", + "titlecase", + "toml", + "trash", + "umask", + "unicode-segmentation", + "unicode-width 0.2.0", + "update-informer", + "ureq", + "url", + "uu_cp", + "uu_mkdir", + "uu_mktemp", + "uu_mv", + "uu_touch", + "uu_uname", + "uu_whoami", + "uucore", + "uuid", + "v_htmlescape", + "wax", + "web-time", + "webpki-roots 1.0.0", + "which", + "windows 0.56.0", + "winreg", +] + +[[package]] +name = "nu-derive-value" +version = "0.105.2" +dependencies = [ + "heck", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "nu-engine" +version = "0.105.2" +dependencies = [ + "log", + "nu-glob", + "nu-path", + "nu-protocol", + "nu-utils", +] + +[[package]] +name = "nu-explore" +version = "0.105.2" +dependencies = [ + "ansi-str", + "anyhow", + "crossterm", + "log", + "lscolors", + "nu-ansi-term", + "nu-color-config", + "nu-engine", + "nu-json", + "nu-parser", + "nu-path", + "nu-pretty-hex", + "nu-protocol", + "nu-table", + "nu-utils", + "ratatui", + "strip-ansi-escapes", + "unicode-width 0.2.0", +] + +[[package]] +name = "nu-glob" +version = "0.105.2" +dependencies = [ + "doc-comment", +] + +[[package]] +name = "nu-json" +version = "0.105.2" +dependencies = [ + "fancy-regex", + "linked-hash-map", + "nu-path", + "nu-test-support", + "num-traits", + "serde", + "serde_json", +] + +[[package]] +name = "nu-lsp" +version = "0.105.2" +dependencies = [ + "assert-json-diff", + "crossbeam-channel", + "lsp-server", + "lsp-textdocument", + "lsp-types", + "memchr", + "miette", + "nu-cli", + "nu-cmd-lang", + "nu-command", + "nu-engine", + "nu-glob", + "nu-parser", + "nu-protocol", + "nu-std", + "nu-test-support", + "nu-utils", + "nucleo-matcher", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "nu-parser" +version = "0.105.2" +dependencies = [ + "bytesize", + "chrono", + "itertools 0.14.0", + "log", + "nu-engine", + "nu-path", + "nu-plugin-engine", + "nu-protocol", + "nu-utils", + "rstest", + "serde_json", +] + +[[package]] +name = "nu-path" +version = "0.105.2" +dependencies = [ + "dirs", + "omnipath", + "pwd", + "ref-cast", +] + +[[package]] +name = "nu-plugin" +version = "0.105.2" +dependencies = [ + "log", + "nix 0.29.0", + "nu-engine", + "nu-plugin-core", + "nu-plugin-protocol", + "nu-protocol", + "nu-utils", + "serde", + "thiserror 2.0.12", + "typetag", +] + +[[package]] +name = "nu-plugin-core" +version = "0.105.2" +dependencies = [ + "interprocess", + "log", + "nu-plugin-protocol", + "nu-protocol", + "rmp-serde", + "serde", + "serde_json", + "windows 0.56.0", +] + +[[package]] +name = "nu-plugin-engine" +version = "0.105.2" +dependencies = [ + "log", + "nu-engine", + "nu-plugin-core", + "nu-plugin-protocol", + "nu-protocol", + "nu-system", + "nu-utils", + "serde", + "typetag", + "windows 0.56.0", +] + +[[package]] +name = "nu-plugin-protocol" +version = "0.105.2" +dependencies = [ + "nu-protocol", + "nu-utils", + "rmp-serde", + "semver", + "serde", + "typetag", +] + +[[package]] +name = "nu-plugin-test-support" +version = "0.105.2" +dependencies = [ + "nu-ansi-term", + "nu-cmd-lang", + "nu-engine", + "nu-parser", + "nu-plugin", + "nu-plugin-core", + "nu-plugin-engine", + "nu-plugin-protocol", + "nu-protocol", + "serde", + "similar", + "typetag", +] + +[[package]] +name = "nu-pretty-hex" +version = "0.105.2" +dependencies = [ + "heapless", + "nu-ansi-term", + "rand 0.8.5", +] + +[[package]] +name = "nu-protocol" +version = "0.105.2" +dependencies = [ + "brotli", + "bytes", + "chrono", + "chrono-humanize", + "dirs", + "dirs-sys", + "fancy-regex", + "heck", + "indexmap", + "log", + "lru", + "memchr", + "miette", + "nix 0.29.0", + "nu-derive-value", + "nu-glob", + "nu-path", + "nu-system", + "nu-test-support", + "nu-utils", + "num-format", + "os_pipe", + "pretty_assertions", + "rmp-serde", + "rstest", + "serde", + "serde_json", + "strum", + "strum_macros", + "tempfile", + "thiserror 2.0.12", + "typetag", + "web-time", + "windows 0.56.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-std" +version = "0.105.2" +dependencies = [ + "log", + "miette", + "nu-engine", + "nu-parser", + "nu-protocol", +] + +[[package]] +name = "nu-system" +version = "0.105.2" +dependencies = [ + "chrono", + "itertools 0.14.0", + "libc", + "libproc", + "log", + "mach2", + "nix 0.29.0", + "ntapi", + "procfs", + "sysinfo", + "web-time", + "windows 0.56.0", +] + +[[package]] +name = "nu-table" +version = "0.105.2" +dependencies = [ + "fancy-regex", + "nu-ansi-term", + "nu-color-config", + "nu-engine", + "nu-protocol", + "nu-utils", + "tabled", +] + +[[package]] +name = "nu-term-grid" +version = "0.105.2" +dependencies = [ + "nu-utils", + "unicode-width 0.2.0", +] + +[[package]] +name = "nu-test-support" +version = "0.105.2" +dependencies = [ + "nu-glob", + "nu-path", + "nu-utils", + "num-format", + "tempfile", + "which", +] + +[[package]] +name = "nu-utils" +version = "0.105.2" +dependencies = [ + "crossterm", + "crossterm_winapi", + "fancy-regex", + "log", + "lscolors", + "nix 0.29.0", + "num-format", + "serde", + "serde_json", + "strip-ansi-escapes", + "sys-locale", + "unicase", +] + +[[package]] +name = "nu_plugin_custom_values" +version = "0.1.0" +dependencies = [ + "nu-plugin", + "nu-plugin-test-support", + "nu-protocol", + "serde", + "typetag", +] + +[[package]] +name = "nu_plugin_example" +version = "0.105.2" +dependencies = [ + "nu-cmd-lang", + "nu-plugin", + "nu-plugin-test-support", + "nu-protocol", +] + +[[package]] +name = "nu_plugin_formats" +version = "0.105.2" +dependencies = [ + "chrono", + "eml-parser", + "ical", + "indexmap", + "nu-plugin", + "nu-plugin-test-support", + "nu-protocol", + "plist", + "rust-ini", +] + +[[package]] +name = "nu_plugin_gstat" +version = "0.105.2" +dependencies = [ + "git2", + "nu-plugin", + "nu-protocol", +] + +[[package]] +name = "nu_plugin_inc" +version = "0.105.2" +dependencies = [ + "nu-plugin", + "nu-protocol", + "semver", +] + +[[package]] +name = "nu_plugin_polars" +version = "0.105.2" +dependencies = [ + "aws-config", + "aws-credential-types", + "chrono", + "chrono-tz", + "env_logger 0.11.5", + "fancy-regex", + "hashbrown 0.15.2", + "indexmap", + "log", + "nu-cmd-lang", + "nu-command", + "nu-engine", + "nu-parser", + "nu-path", + "nu-plugin", + "nu-plugin-test-support", + "nu-protocol", + "nu-utils", + "num", + "object_store", + "polars", + "polars-arrow", + "polars-io", + "polars-ops", + "polars-plan", + "polars-utils", + "serde", + "sqlparser", + "tempfile", + "tokio", + "typetag", + "url", + "uuid", +] + +[[package]] +name = "nu_plugin_query" +version = "0.105.2" +dependencies = [ + "gjson", + "nu-plugin", + "nu-protocol", + "scraper", + "serde", + "serde_json", + "sxd-document", + "sxd-xpath", + "webpage", +] + +[[package]] +name = "nu_plugin_stress_internals" +version = "0.105.2" +dependencies = [ + "interprocess", + "serde", + "serde_json", +] + +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "nuon" +version = "0.105.2" +dependencies = [ + "chrono", + "nu-engine", + "nu-parser", + "nu-protocol", + "nu-utils", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "object_store" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d94ac16b433c0ccf75326388c893d2835ab7457ea35ab8ba5d745c053ef5fa16" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "chrono", + "form_urlencoded", + "futures", + "http 1.2.0", + "http-body-util", + "humantime", + "hyper 1.5.1", + "itertools 0.14.0", + "md-5", + "parking_lot", + "percent-encoding", + "quick-xml 0.37.1", + "rand 0.9.0", + "reqwest", + "ring", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror 2.0.12", + "tokio", + "tracing", + "url", + "walkdir", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "oem_cp" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330138902ab4dab09a86e6b7ab7ddeffb5f8435d52fe0df1bce8b06a17b10ee4" +dependencies = [ + "phf", + "phf_codegen", + "serde", + "serde_json", +] + +[[package]] +name = "omnipath" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80adb31078122c880307e9cdfd4e3361e6545c319f9b9dcafcb03acd3b51a575" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "open" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.4.1+3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "os_display" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" +dependencies = [ + "unicode-width 0.1.11", +] + +[[package]] +name = "os_pipe" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + +[[package]] +name = "owo-colors" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" + +[[package]] +name = "papergrid" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1" +dependencies = [ + "ansi-str", + "ansitok", + "bytecount", + "fnv", + "unicode-width 0.2.0", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "parse_datetime" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bffd1156cebf13f681d7769924d3edfb9d9d71ba206a8d8e8e7eb9df4f4b1e7" +dependencies = [ + "chrono", + "nom 8.0.0", + "regex", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "peresil" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f658886ed52e196e850cfbbfddab9eaa7f6d90dd0929e264c31e5cec07e09e57" + +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_consume" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79447402d15d18e7142e14c72f2e63fa3d155be1bc5b70b3ccbb610ac55f536b" +dependencies = [ + "pest", + "pest_consume_macros", + "pest_derive", +] + +[[package]] +name = "pest_consume_macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8630a7a899cb344ec1c16ba0a6b24240029af34bdc0a21f84e411d7f793f29" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "planus" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3daf8e3d4b712abe1d690838f6e29fb76b76ea19589c4afa39ec30e12f62af71" +dependencies = [ + "array-init-cursor", + "hashbrown 0.15.2", +] + +[[package]] +name = "platform-info" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91077ffd05d058d70d79eefcd7d7f6aac34980860a7519960f7913b6563a8c3a" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml 0.32.0", + "serde", + "time", +] + +[[package]] +name = "polars" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443824f43bca39b178353d6c09e4b44e115b21f107a5654d5f980d20b432a303" +dependencies = [ + "getrandom 0.2.15", + "polars-arrow", + "polars-core", + "polars-error", + "polars-io", + "polars-lazy", + "polars-ops", + "polars-parquet", + "polars-sql", + "polars-time", + "polars-utils", + "version_check", +] + +[[package]] +name = "polars-arrow" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809c5340e9e6c16eee5a07585161bae99f903f53af7402075efec23ee75fce5b" +dependencies = [ + "atoi_simd", + "avro-schema", + "bitflags 2.6.0", + "bytemuck", + "chrono", + "chrono-tz", + "dyn-clone", + "either", + "ethnum", + "getrandom 0.2.15", + "hashbrown 0.15.2", + "itoa", + "lz4", + "num-traits", + "polars-arrow-format", + "polars-error", + "polars-schema", + "polars-utils", + "serde", + "simdutf8", + "streaming-iterator", + "strum_macros", + "version_check", + "zstd", +] + +[[package]] +name = "polars-arrow-format" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863c04c514be005eced7db7053e20d49f7e7a58048a282fa52dfea1fd5434e78" +dependencies = [ + "planus", + "serde", +] + +[[package]] +name = "polars-compute" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8802ff2cccea01a845ea8267a7600e495747ed109035bb5020c33eb8717ff4" +dependencies = [ + "atoi_simd", + "bytemuck", + "chrono", + "either", + "fast-float2", + "hashbrown 0.15.2", + "itoa", + "num-traits", + "polars-arrow", + "polars-error", + "polars-utils", + "rand 0.8.5", + "ryu", + "serde", + "skiplist", + "strength_reduce", + "strum_macros", + "version_check", +] + +[[package]] +name = "polars-core" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc3c99d7000be1be11665e1e260b93dc3b927342b9da3b53d9a1ac264e4343d" +dependencies = [ + "bitflags 2.6.0", + "boxcar", + "bytemuck", + "chrono", + "chrono-tz", + "comfy-table", + "either", + "hashbrown 0.14.5", + "hashbrown 0.15.2", + "indexmap", + "itoa", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-error", + "polars-row", + "polars-schema", + "polars-utils", + "rand 0.8.5", + "rand_distr", + "rayon", + "regex", + "serde", + "serde_json", + "strum_macros", + "uuid", + "version_check", + "xxhash-rust", +] + +[[package]] +name = "polars-error" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1397c17712e61a55fdd45c033a69f0451fde2973ff2609c22e363e21d68f11ef" +dependencies = [ + "avro-schema", + "object_store", + "parking_lot", + "polars-arrow-format", + "regex", + "signal-hook", + "simdutf8", +] + +[[package]] +name = "polars-expr" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d3aa6722c9a3e0b721ec2bcdc4affd9e50e4cb606cd81bb94535a9a5a6ade9" +dependencies = [ + "bitflags 2.6.0", + "hashbrown 0.15.2", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-io", + "polars-ops", + "polars-plan", + "polars-row", + "polars-time", + "polars-utils", + "rand 0.8.5", + "rayon", + "recursive", +] + +[[package]] +name = "polars-io" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a632d442a99821250a8fa66f7d488bf5ee98e5f515e65256b12956cb81fc110" +dependencies = [ + "async-trait", + "atoi_simd", + "blake3", + "bytes", + "chrono", + "chrono-tz", + "fast-float2", + "flate2", + "fs4", + "futures", + "glob", + "hashbrown 0.15.2", + "home", + "itoa", + "memchr", + "memmap2", + "num-traits", + "object_store", + "percent-encoding", + "polars-arrow", + "polars-core", + "polars-error", + "polars-json", + "polars-parquet", + "polars-schema", + "polars-time", + "polars-utils", + "rayon", + "regex", + "reqwest", + "ryu", + "serde", + "serde_json", + "simd-json", + "simdutf8", + "tokio", + "tokio-util", + "url", + "zstd", +] + +[[package]] +name = "polars-json" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd891735404ebb9d6ace066cfb4b8f6edb321bc841d354a0d917a3a1f2d1ca5b" +dependencies = [ + "chrono", + "chrono-tz", + "fallible-streaming-iterator", + "hashbrown 0.15.2", + "indexmap", + "itoa", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-error", + "polars-utils", + "ryu", + "simd-json", + "streaming-iterator", +] + +[[package]] +name = "polars-lazy" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ed0c87bdc8820447a38ae8efdb5a51a5a93e8bd528cffb05d05cf1145e4161" +dependencies = [ + "bitflags 2.6.0", + "chrono", + "either", + "futures", + "memchr", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-expr", + "polars-io", + "polars-json", + "polars-mem-engine", + "polars-ops", + "polars-plan", + "polars-stream", + "polars-time", + "polars-utils", + "rayon", + "tokio", + "version_check", +] + +[[package]] +name = "polars-mem-engine" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675294ddf9174029e48caa4e59b0665ea64bfb784a366b197690895a6ed65c68" +dependencies = [ + "futures", + "memmap2", + "polars-arrow", + "polars-core", + "polars-error", + "polars-expr", + "polars-io", + "polars-json", + "polars-ops", + "polars-plan", + "polars-time", + "polars-utils", + "rayon", + "recursive", + "tokio", +] + +[[package]] +name = "polars-ops" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eb4db68956f857c52eeda072d87644a7b42eac41d55073af94dfac8441af6cf" +dependencies = [ + "argminmax", + "base64 0.22.1", + "bytemuck", + "chrono", + "chrono-tz", + "either", + "hashbrown 0.15.2", + "hex", + "indexmap", + "jsonpath_lib_polars_vendor", + "libm", + "memchr", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-json", + "polars-schema", + "polars-utils", + "rand 0.8.5", + "rand_distr", + "rayon", + "regex", + "regex-syntax", + "serde", + "serde_json", + "strum_macros", + "unicode-normalization", + "unicode-reverse", + "version_check", +] + +[[package]] +name = "polars-parquet" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c849c10edd9511ccd4ec4130e283ee3a8b3bb48a7d74ac6354c1c20add81065" +dependencies = [ + "async-stream", + "base64 0.22.1", + "brotli", + "bytemuck", + "ethnum", + "flate2", + "futures", + "hashbrown 0.15.2", + "lz4", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-error", + "polars-parquet-format", + "polars-utils", + "serde", + "simdutf8", + "snap", + "streaming-decompression", + "zstd", +] + +[[package]] +name = "polars-parquet-format" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c025243dcfe8dbc57e94d9f82eb3bef10b565ab180d5b99bed87fd8aea319ce1" +dependencies = [ + "async-trait", + "futures", +] + +[[package]] +name = "polars-plan" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fb4412c42bf637c2c02a617381c682ed425d9c8e4bd1fcb85cf352ed2a67c6" +dependencies = [ + "bitflags 2.6.0", + "bytemuck", + "bytes", + "chrono", + "chrono-tz", + "either", + "futures", + "hashbrown 0.15.2", + "memmap2", + "num-traits", + "percent-encoding", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-io", + "polars-json", + "polars-ops", + "polars-parquet", + "polars-time", + "polars-utils", + "rayon", + "recursive", + "regex", + "serde", + "strum_macros", + "version_check", +] + +[[package]] +name = "polars-row" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08fb77ac1d37340d9cfe57cf58000cf3d9cce429e10d25066952c6145c684cc0" +dependencies = [ + "bitflags 2.6.0", + "bytemuck", + "polars-arrow", + "polars-compute", + "polars-error", + "polars-utils", +] + +[[package]] +name = "polars-schema" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada7c7e2fbbeffbdd67628cd8a89f02b0a8d21c71d34e297e2463a7c17575203" +dependencies = [ + "indexmap", + "polars-error", + "polars-utils", + "serde", + "version_check", +] + +[[package]] +name = "polars-sql" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a8e512b1f05ffda9963fe8f6a7c62dcba86be85218bc033ecdad2802cc1b1a0" +dependencies = [ + "bitflags 2.6.0", + "hex", + "polars-core", + "polars-error", + "polars-lazy", + "polars-ops", + "polars-plan", + "polars-time", + "polars-utils", + "rand 0.8.5", + "regex", + "serde", + "sqlparser", +] + +[[package]] +name = "polars-stream" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0a02d8050acd9b64ed7e36c5bc96f6d4f46a940220f9c0e34c96b51f830f8c" +dependencies = [ + "async-channel", + "async-trait", + "atomic-waker", + "bitflags 2.6.0", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "futures", + "memmap2", + "parking_lot", + "percent-encoding", + "pin-project-lite", + "polars-arrow", + "polars-core", + "polars-error", + "polars-expr", + "polars-io", + "polars-mem-engine", + "polars-ops", + "polars-parquet", + "polars-plan", + "polars-utils", + "rand 0.8.5", + "rayon", + "recursive", + "slotmap", + "tokio", + "version_check", +] + +[[package]] +name = "polars-time" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e84a30110880ffede8d93c085fc429ab1b8bf1acf3d6d489143dd34be374c4" +dependencies = [ + "atoi_simd", + "bytemuck", + "chrono", + "chrono-tz", + "now", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-ops", + "polars-utils", + "rayon", + "regex", + "serde", + "strum_macros", +] + +[[package]] +name = "polars-utils" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05e033960552c47fc35afe14d5af5b29696acc97ae5d3c585ebc33c246cc15f" +dependencies = [ + "bincode", + "bytemuck", + "bytes", + "compact_str", + "flate2", + "foldhash", + "hashbrown 0.15.2", + "indexmap", + "libc", + "memmap2", + "num-traits", + "polars-error", + "rand 0.8.5", + "raw-cpuid", + "rayon", + "regex", + "rmp-serde", + "serde", + "serde_json", + "slotmap", + "stacker", + "version_check", +] + +[[package]] +name = "pori" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a63d338dec139f56dacc692ca63ad35a6be6a797442479b55acd611d79e906" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "print-positions" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df593470e3ef502e48cb0cfc9a3a61e5f61e967b78e1ed35a67ac615cfbd208" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags 2.6.0", + "chrono", + "flate2", + "hex", + "procfs-core", + "rustix 0.38.42", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.6.0", + "chrono", + "hex", +] + +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + +[[package]] +name = "pure-rust-locales" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1190fd18ae6ce9e137184f207593877e70f39b015040156b1e05081cdfe3733a" + +[[package]] +name = "pwd" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c71c0c79b9701efe4e1e4b563b2016dd4ee789eb99badcb09d61ac4b92e4a2" +dependencies = [ + "libc", + "thiserror 1.0.69", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" +dependencies = [ + "encoding_rs", + "memchr", + "serde", +] + +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger 0.8.4", + "log", + "rand 0.8.5", +] + +[[package]] +name = "quickcheck_macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.0", + "rustls 0.23.20", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom 0.2.15", + "rand 0.8.5", + "ring", + "rustc-hash 2.1.0", + "rustls 0.23.20", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.23", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "recursive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = [ + "recursive-proc-macro-impl", + "stacker", +] + +[[package]] +name = "recursive-proc-macro-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = [ + "quote", + "syn 2.0.90", +] + +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "reedline" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5cdfab7494d13ebfb6ce64828648518205d3ce8541ef1f94a27887f29d2d50b" +dependencies = [ + "arboard", + "chrono", + "crossterm", + "fd-lock", + "itertools 0.13.0", + "nu-ansi-term", + "rusqlite", + "serde", + "serde_json", + "strip-ansi-escapes", + "strum", + "strum_macros", + "thiserror 2.0.12", + "unicode-segmentation", + "unicode-width 0.2.0", +] + +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-rustls 0.27.3", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.20", + "rustls-native-certs 0.8.1", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.1", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.26.8", + "windows-registry", +] + +[[package]] +name = "rfc2047-decoder" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc36545d1021456a751b573517cb52e8c339b2f662e6b2778ef629282678de29" +dependencies = [ + "base64 0.22.1", + "charset", + "chumsky", + "memchr", + "quoted_printable", + "thiserror 2.0.12", +] + +[[package]] +name = "ring" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.90", + "unicode-ident", +] + +[[package]] +name = "rstest_reuse" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" +dependencies = [ + "quote", + "rand 0.8.5", + "syn 2.0.90", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.6.0", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust-embed" +version = "8.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.90", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.0.1", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scc" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1" +dependencies = [ + "sdd", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e65d9d888567588db4c12da1087598d0f6f8b346cc2c5abc91f05fc2dffe2" +dependencies = [ + "cssparser", + "ego-tree", + "html5ever 0.29.0", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sdd" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07779b9b918cc05650cb30f404d4d7835d26df37c235eded8a6832e2fb82cca" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags 2.6.0", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "servo_arc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shadow-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6fd27df794ced2ef39872879c93a9f87c012607318af8621cd56d2c3a8b3a2" +dependencies = [ + "const_format", + "is_debug", + "time", + "tzdb", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 1.0.3", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd-json" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2bcf6c6e164e81bc7a5d49fc6988b3d515d9e8c07457d7b74ffb9324b9cd40" +dependencies = [ + "ahash", + "getrandom 0.2.15", + "halfbrown", + "once_cell", + "ref-cast", + "serde", + "serde_json", + "simdutf8", + "value-trait", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "skiplist" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eec25f46463fcdc5e02f388c2780b1b58e01be81a8378e62ec60931beccc3f6" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "sqlparser" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05a528114c392209b3264855ad491fcce534b94a38771b0a0b97a79379275ce8" +dependencies = [ + "log", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "streaming-decompression" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6cc3b19bfb128a8ad11026086e31d3ce9ad23f8ea37354b31383a187c44cf3" +dependencies = [ + "fallible-streaming-iterator", +] + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte 0.11.1", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.90", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "sxd-document" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d82f37be9faf1b10a82c4bd492b74f698e40082f0f40de38ab275f31d42078" +dependencies = [ + "peresil", + "typed-arena", +] + +[[package]] +name = "sxd-xpath" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e39da5d30887b5690e29de4c5ebb8ddff64ebd9933f98a01daaa4fd11b36ea" +dependencies = [ + "peresil", + "quick-error 1.2.3", + "sxd-document", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "tabled" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d" +dependencies = [ + "ansi-str", + "ansitok", + "papergrid", + "testing_table", +] + +[[package]] +name = "tango-bench" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257822358c6f206fed78bfe6369cf959063b0644d70f88df6b19f2dadc93423e" +dependencies = [ + "alloca", + "anyhow", + "clap", + "colorz", + "glob-match", + "goblin", + "libloading", + "log", + "num-traits", + "rand 0.8.5", + "scroll", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +dependencies = [ + "rustix 0.38.42", + "windows-sys 0.59.0", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "testing_table" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" +dependencies = [ + "ansitok", + "unicode-width 0.2.0", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.1.11", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "titlecase" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb567088a91d59b492520c8149e2be5ce10d5deb2d9a383f3378df3259679d40" +dependencies = [ + "regex", +] + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 1.0.3", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls 0.23.20", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "trash" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e5ca62c20366b4685e3e41fba17bc7c9bbdcb82e65a89d6fda2ceea5fffd2f" +dependencies = [ + "chrono", + "libc", + "log", + "objc2", + "objc2-foundation", + "once_cell", + "percent-encoding", + "scopeguard", + "urlencoding", + "windows 0.56.0", +] + +[[package]] +name = "tree_magic_mini" +version = "3.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" +dependencies = [ + "fnv", + "memchr", + "nom 7.1.3", + "once_cell", + "petgraph", +] + +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-arena" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" + +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "typetag" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba3b6e86ffe0054b2c44f2d86407388b933b16cb0a70eea3929420db1d9bbe" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b20a22c42c8f1cd23ce5e34f165d4d37038f5b663ad20fb6adbdf029172483" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "tz-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1450bf2b99397e72070e7935c89facaa80092ac812502200375f1f7d33c71a1" + +[[package]] +name = "tzdb" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2ea5956f295449f47c0b825c5e109022ff1a6a53bb4f77682a87c2341fbf5" +dependencies = [ + "iana-time-zone", + "tz-rs", + "tzdb_data", +] + +[[package]] +name = "tzdb_data" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0604b35c1f390a774fdb138cac75a99981078895d24bcab175987440bbff803b" +dependencies = [ + "tz-rs", +] + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "umask" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9a46c2549e35c054e0ffe281a3a6ec0007793db4df106604d37ed3f4d73d1c" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-reverse" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6f4888ebc23094adfb574fdca9fdc891826287a6397d2cd28802ffd6f20c76" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.11", +] + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "update-informer" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53813bf5d5f0d8430794f8cc48e99521cc9e298066958d16383ccb8b39d182a7" +dependencies = [ + "etcetera", + "reqwest", + "semver", + "serde", + "serde_json", + "ureq", +] + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "encoding_rs", + "flate2", + "log", + "native-tls", + "once_cell", + "rustls 0.23.20", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "url", + "webpki-roots 0.26.8", +] + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uu_cp" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf2f3906b7896f79519055d36760095577373e40ec244f46b259f502a4a91147" +dependencies = [ + "clap", + "filetime", + "indicatif", + "libc", + "quick-error 2.0.1", + "uucore", + "walkdir", + "xattr", +] + +[[package]] +name = "uu_mkdir" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be556a5d852f55b92bba460d7a97030a340ba4a3f4c510a8d0a893bfaf48356" +dependencies = [ + "clap", + "uucore", +] + +[[package]] +name = "uu_mktemp" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5305fcf4f7f480e7438e19ff433ae60dea886bd528f87543029eb6b95d351afc" +dependencies = [ + "clap", + "rand 0.9.0", + "tempfile", + "thiserror 2.0.12", + "uucore", +] + +[[package]] +name = "uu_mv" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be214b96554e4f7aa079b26c86c3ecf1b9ea15023ca2ec62d608273d12c7049" +dependencies = [ + "clap", + "fs_extra", + "indicatif", + "libc", + "thiserror 2.0.12", + "uucore", + "windows-sys 0.59.0", +] + +[[package]] +name = "uu_touch" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e58581a0245de8e3ef75b115ab29592cfb60d4851149d4951604601d14ea420" +dependencies = [ + "chrono", + "clap", + "filetime", + "parse_datetime", + "thiserror 2.0.12", + "uucore", + "windows-sys 0.59.0", +] + +[[package]] +name = "uu_uname" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324d96a21da91a81be334206ab65aad16d164d34cddeb640e1c56cd8d1854dd4" +dependencies = [ + "clap", + "platform-info", + "uucore", +] + +[[package]] +name = "uu_whoami" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee254de8b172a5978f12fe6cd9d4f2b60ea9ef1e37f0cb53bfee2c993b3e96a" +dependencies = [ + "clap", + "libc", + "uucore", + "windows-sys 0.59.0", +] + +[[package]] +name = "uucore" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71f4e82877d06de779c611a3d54720f56f1e68b228fb30a5b6c66ef07e68263d" +dependencies = [ + "chrono", + "chrono-tz", + "clap", + "dunce", + "glob", + "iana-time-zone", + "libc", + "nix 0.29.0", + "number_prefix", + "os_display", + "uucore_procs", + "walkdir", + "wild", + "winapi-util", + "windows-sys 0.59.0", + "xattr", +] + +[[package]] +name = "uucore_procs" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72435859e812e602e225dea48d014abb6b1072220a8d44f2fe0565553b1f7e4" +dependencies = [ + "proc-macro2", + "quote", + "uuhelp_parser", +] + +[[package]] +name = "uuhelp_parser" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb6d972f580f8223cb7052d8580aea2b7061e368cf476de32ea9457b19459ed" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "atomic", + "getrandom 0.3.1", + "js-sys", + "md-5", + "serde", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + +[[package]] +name = "value-trait" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9170e001f458781e92711d2ad666110f153e4e50bfd5cbd02db6547625714187" +dependencies = [ + "float-cmp", + "halfbrown", + "itoa", + "ryu", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "arrayvec", + "memchr", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wax" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d12a78aa0bab22d2f26ed1a96df7ab58e8a93506a3e20adb47c51a93b4e1357" +dependencies = [ + "const_format", + "itertools 0.11.0", + "nom 7.1.3", + "pori", + "regex", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "wayland-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.42", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" +dependencies = [ + "bitflags 2.6.0", + "rustix 0.38.42", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +dependencies = [ + "proc-macro2", + "quick-xml 0.36.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpage" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" +dependencies = [ + "curl", + "html5ever 0.27.0", + "markup5ever_rcdom", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix 1.0.7", + "winsafe", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "wild" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" +dependencies = [ + "glob", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winresource" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7276691b353ad4547af8c3268488d1311f4be791ffdc0c65b8cfa8f41eed693b" +dependencies = [ + "toml", + "version_check", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" +dependencies = [ + "derive-new", + "libc", + "log", + "nix 0.28.0", + "os_pipe", + "tempfile", + "thiserror 1.0.69", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.42", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.42", +] + +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "xxhash-rust" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "zip" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/nushell/Cargo.toml b/nushell/Cargo.toml new file mode 100644 index 0000000..6e39971 --- /dev/null +++ b/nushell/Cargo.toml @@ -0,0 +1,344 @@ +[package] +authors = ["The Nushell Project Developers"] +build = "scripts/build.rs" +default-run = "nu" +description = "A new type of shell" +documentation = "https://www.nushell.sh/book/" +edition = "2024" +exclude = ["images"] +homepage = "https://www.nushell.sh" +license = "MIT" +name = "nu" +repository = "https://github.com/nushell/nushell" +#rust-version = "1.85.1" +version = "0.105.2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/{ version }/{ name }-{ version }-{ target }.{ archive-format }" +pkg-fmt = "tgz" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" + +[workspace] +members = [ + "crates/nu-cli", + "crates/nu-engine", + "crates/nu-parser", + "crates/nu-system", + "crates/nu-cmd-base", + "crates/nu-cmd-extra", + "crates/nu-cmd-lang", + "crates/nu-cmd-plugin", + "crates/nu-command", + "crates/nu-color-config", + "crates/nu-explore", + "crates/nu-json", + "crates/nu-lsp", + "crates/nu-pretty-hex", + "crates/nu-protocol", + "crates/nu-derive-value", + "crates/nu-plugin", + "crates/nu-plugin-core", + "crates/nu-plugin-engine", + "crates/nu-plugin-protocol", + "crates/nu-plugin-test-support", + "crates/nu_plugin_inc", + "crates/nu_plugin_gstat", + "crates/nu_plugin_example", + "crates/nu_plugin_query", + "crates/nu_plugin_custom_values", + "crates/nu_plugin_formats", + "crates/nu_plugin_polars", + "crates/nu_plugin_stress_internals", + "crates/nu-std", + "crates/nu-table", + "crates/nu-term-grid", + "crates/nu-test-support", + "crates/nu-utils", + "crates/nuon", +] + +[workspace.dependencies] +alphanumeric-sort = "1.5" +ansi-str = "0.9" +anyhow = "1.0.82" +base64 = "0.22.1" +bracoxide = "0.1.6" +brotli = "7.0" +byteorder = "1.5" +bytes = "1" +bytesize = "1.3.3" +calamine = "0.28" +chardetng = "0.1.17" +chrono = { default-features = false, version = "0.4.34" } +chrono-humanize = "0.2.3" +chrono-tz = "0.10" +crossbeam-channel = "0.5.8" +crossterm = "0.28.1" +csv = "1.3" +ctrlc = "3.4" +devicons = "0.6.12" +dialoguer = { default-features = false, version = "0.11" } +digest = { default-features = false, version = "0.10" } +dirs = "5.0" +dirs-sys = "0.4" +dtparse = "2.0" +encoding_rs = "0.8" +fancy-regex = "0.14" +filesize = "0.2" +filetime = "0.2" +heck = "0.5.0" +human-date-parser = "0.3.0" +indexmap = "2.9" +indicatif = "0.17" +interprocess = "2.2.0" +is_executable = "1.0" +itertools = "0.14" +libc = "0.2" +libproc = "0.14" +log = "0.4" +lru = "0.12" +lscolors = { version = "0.20", default-features = false } +lsp-server = "0.7.8" +lsp-types = { version = "0.97.0", features = ["proposed"] } +lsp-textdocument = "0.4.2" +mach2 = "0.4" +md5 = { version = "0.10", package = "md-5" } +miette = "7.6" +mime = "0.3.17" +mime_guess = "2.0" +mockito = { version = "1.7", default-features = false } +multipart-rs = "0.1.13" +native-tls = "0.2" +nix = { version = "0.29", default-features = false } +notify-debouncer-full = { version = "0.3", default-features = false } +nu-ansi-term = "0.50.1" +nucleo-matcher = "0.3" +num-format = "0.4" +num-traits = "0.2" +oem_cp = "2.0.0" +omnipath = "0.1" +open = "5.3" +os_pipe = { version = "1.2", features = ["io_safety"] } +pathdiff = "0.2" +percent-encoding = "2" +pretty_assertions = "1.4" +print-positions = "0.6" +proc-macro-error2 = "2.0" +proc-macro2 = "1.0" +procfs = "0.17.0" +pwd = "1.3" +quick-xml = "0.37.0" +quickcheck = "1.0" +quickcheck_macros = "1.1" +quote = "1.0" +rand = "0.9" +getrandom = "0.2" # pick same version that rand requires +rand_chacha = "0.9" +ratatui = "0.29" +rayon = "1.10" +reedline = "0.40.0" +rmp = "0.8" +rmp-serde = "1.3" +roxmltree = "0.20" +rstest = { version = "0.23", default-features = false } +rstest_reuse = "0.7" +rusqlite = "0.31" +rust-embed = "8.7.0" +rustls = { version = "0.23", default-features = false, features = ["std", "tls12"] } +rustls-native-certs = "0.8" +scopeguard = { version = "1.2.0" } +serde = { version = "1.0" } +serde_json = "1.0.97" +serde_urlencoded = "0.7.1" +serde_yaml = "0.9.33" +sha2 = "0.10" +strip-ansi-escapes = "0.2.0" +strum = "0.26" +strum_macros = "0.26" +syn = "2.0" +sysinfo = "0.33" +tabled = { version = "0.20", default-features = false } +tempfile = "3.20" +titlecase = "3.6" +toml = "0.8" +trash = "5.2" +update-informer = { version = "1.2.0", default-features = false, features = ["github", "ureq"] } +umask = "2.1" +unicode-segmentation = "1.12" +unicode-width = "0.2" +ureq = { version = "2.12", default-features = false, features = ["socks-proxy"] } +url = "2.2" +uu_cp = "0.0.30" +uu_mkdir = "0.0.30" +uu_mktemp = "0.0.30" +uu_mv = "0.0.30" +uu_touch = "0.0.30" +uu_whoami = "0.0.30" +uu_uname = "0.0.30" +uucore = "0.0.30" +uuid = "1.16.0" +v_htmlescape = "0.15.0" +wax = "0.6" +web-time = "1.1.0" +which = "7.0.3" +windows = "0.56" +windows-sys = "0.48" +winreg = "0.52" +memchr = "2.7.4" +webpki-roots = "1.0" + +[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" +used_underscore_binding = "warn" + +[lints] +workspace = true + +[dependencies] +nu-cli = { path = "./crates/nu-cli", version = "0.105.2" } +nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.105.2" } +nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.105.2" } +nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.105.2", optional = true } +nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.105.2" } +nu-command = { path = "./crates/nu-command", version = "0.105.2", default-features = false, features = ["os"] } +nu-engine = { path = "./crates/nu-engine", version = "0.105.2" } +nu-explore = { path = "./crates/nu-explore", version = "0.105.2" } +nu-lsp = { path = "./crates/nu-lsp/", version = "0.105.2" } +nu-parser = { path = "./crates/nu-parser", version = "0.105.2" } +nu-path = { path = "./crates/nu-path", version = "0.105.2" } +nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.105.2" } +nu-protocol = { path = "./crates/nu-protocol", version = "0.105.2" } +nu-std = { path = "./crates/nu-std", version = "0.105.2" } +nu-system = { path = "./crates/nu-system", version = "0.105.2" } +nu-utils = { path = "./crates/nu-utils", version = "0.105.2" } +reedline = { workspace = true, features = ["bashisms", "sqlite"] } + +crossterm = { workspace = true } +ctrlc = { workspace = true } +dirs = { workspace = true } +log = { workspace = true } +miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] } +multipart-rs = { workspace = true } +serde_json = { workspace = true } +simplelog = "0.12" +time = "0.3" + +[target.'cfg(not(target_os = "windows"))'.dependencies] +# Our dependencies don't use OpenSSL on Windows +openssl = { version = "0.10", features = ["vendored"], optional = true } + +[target.'cfg(windows)'.build-dependencies] +winresource = "0.1" + +[target.'cfg(target_family = "unix")'.dependencies] +nix = { workspace = true, default-features = false, features = [ + "signal", + "process", + "fs", + "term", +] } + +[dev-dependencies] +nu-test-support = { path = "./crates/nu-test-support", version = "0.105.2" } +nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.105.2" } +nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.105.2" } +assert_cmd = "2.0" +dirs = { workspace = true } +tango-bench = "0.6" +pretty_assertions = { workspace = true } +fancy-regex = { workspace = true } +rstest = { workspace = true, default-features = false } +serial_test = "3.2" +tempfile = { workspace = true } + +[features] +# Enable all features while still avoiding mutually exclusive features. +# Use this if `--all-features` fails. +full = ["plugin", "rustls-tls", "system-clipboard", "trash-support", "sqlite"] + +plugin = [ + # crates + "dep:nu-cmd-plugin", + "dep:nu-plugin-engine", + + # features + "nu-cli/plugin", + "nu-cmd-lang/plugin", + "nu-command/plugin", + "nu-engine/plugin", + "nu-engine/plugin", + "nu-parser/plugin", + "nu-protocol/plugin", +] + +native-tls = ["nu-command/native-tls"] +rustls-tls = ["nu-command/rustls-tls"] + +default = [ + "plugin", + "trash-support", + "sqlite", + "rustls-tls" +] +stable = ["default"] +# 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/); +# otherwise the system version will be used. Not enabled by default because it takes a while to build +static-link-openssl = ["dep:openssl"] + +# Optional system clipboard support in `reedline`, this behavior has problematic compatibility with some systems. +# Missing X server/ Wayland can cause issues +system-clipboard = [ + "reedline/system_clipboard", + "nu-cli/system-clipboard", +] + +# Stable (Default) +trash-support = ["nu-command/trash-support"] + +# SQLite commands for nushell +sqlite = ["nu-command/sqlite", "nu-std/sqlite"] + +[profile.release] +opt-level = "s" # Optimize for size +strip = "debuginfo" +lto = "thin" + +# build with `cargo build --profile profiling` +# to analyze performance with tooling like linux perf +[profile.profiling] +inherits = "release" +strip = false +debug = true + +# build with `cargo build --profile ci` +# to analyze performance with tooling like linux perf +[profile.ci] +inherits = "dev" +strip = false +debug = false + +# Main nu binary +[[bin]] +name = "nu" +path = "src/main.rs" +bench = false + +# To use a development version of a dependency please use a global override here +# changing versions in each sub-crate of the workspace is tedious +[patch.crates-io] +# reedline = { git = "https://github.com/nushell/reedline", branch = "main" } +# nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"} + +# Run all benchmarks with `cargo bench` +# Run individual benchmarks like `cargo bench -- ` e.g. `cargo bench -- parse` +[[bench]] +name = "benchmarks" +harness = false diff --git a/nushell/Cross.toml b/nushell/Cross.toml new file mode 100644 index 0000000..804afeb --- /dev/null +++ b/nushell/Cross.toml @@ -0,0 +1,18 @@ +# Configuration for cross-rs: https://github.com/cross-rs/cross +# Run cross-rs like this: +# cross build --target aarch64-unknown-linux-gnu --release +# or +# cross build --target aarch64-unknown-linux-musl --release --features=static-link-openssl + +[target.aarch64-unknown-linux-gnu] +pre-build = [ + "dpkg --add-architecture $CROSS_DEB_ARCH", + "apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH clang" +] + +# NOTE: for musl you will need to build with --features=static-link-openssl +[target.aarch64-unknown-linux-musl] +pre-build = [ + "dpkg --add-architecture $CROSS_DEB_ARCH", + "apt-get update && apt-get install --assume-yes clang" +] diff --git a/nushell/LICENSE b/nushell/LICENSE new file mode 100644 index 0000000..1ecbafd --- /dev/null +++ b/nushell/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2025 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nushell/README.md b/nushell/README.md new file mode 100644 index 0000000..ff5ffe4 --- /dev/null +++ b/nushell/README.md @@ -0,0 +1,237 @@ +# Nushell +[![Crates.io](https://img.shields.io/crates/v/nu.svg)](https://crates.io/crates/nu) +[![Build Status](https://img.shields.io/github/actions/workflow/status/nushell/nushell/ci.yml?branch=main)](https://github.com/nushell/nushell/actions) +[![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) +[![The Changelog #363](https://img.shields.io/badge/The%20Changelog-%23363-61c192.svg)](https://changelog.com/podcast/363) +[![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) + +A new type of shell. + +![Example of nushell](assets/nushell-autocomplete6.gif "Example of nushell") + +## Table of Contents + +- [Status](#status) +- [Learning About Nu](#learning-about-nu) +- [Installation](#installation) +- [Configuration](#configuration) +- [Philosophy](#philosophy) + - [Pipelines](#pipelines) + - [Opening files](#opening-files) + - [Plugins](#plugins) +- [Goals](#goals) +- [Officially Supported By](#officially-supported-by) +- [Contributing](#contributing) +- [License](#license) + +## Status + +This project has reached a minimum-viable-product level of quality. Many people use it as their daily driver, but it may be unstable for some commands. Nu's design is subject to change as it matures. + +## Learning About Nu + +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! + +## Installation + +To quickly install Nu: + +```bash +# Linux and macOS +brew install nushell +# Windows +winget install nushell +``` + +To use `Nu` in GitHub Action, check [setup-nu](https://github.com/marketplace/actions/setup-nu) for more detail. + +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) + +For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md). + +## Configuration + +The default configurations can be found at [sample_config](crates/nu-utils/src/default_files) +which are the configuration files one gets when they startup Nushell for the first time. + +It sets all of the default configuration to run Nushell. From here one can +then customize this file for their specific needs. + +To see where *config.nu* is located on your system simply type this command. + +```rust +$nu.config-path +``` + +Please see our [book](https://www.nushell.sh) for all of the Nushell documentation. + + +## Philosophy + +Nu draws inspiration from projects like PowerShell, functional programming languages, and modern CLI tools. +Rather than thinking of files and data as raw streams of text, Nu looks at each input as something with structure. +For example, when you list the contents of a directory what you get back is a table of rows, where each row represents an item in that directory. +These values can be piped through a series of steps, in a series of commands called a 'pipeline'. + +### Pipelines + +In Unix, it's common to pipe between commands to split up a sophisticated command over multiple steps. +Nu takes this a step further and builds heavily on the idea of _pipelines_. +As in the Unix philosophy, Nu allows commands to output to stdout and read from stdin. +Additionally, commands can output structured data (you can think of this as a third kind of stream). +Commands that work in the pipeline fit into one of three categories: + +- Commands that produce a stream (e.g., `ls`) +- Commands that filter a stream (e.g., `where type == "dir"`) +- Commands that consume the output of the pipeline (e.g., `table`) + +Commands are separated by the pipe symbol (`|`) to denote a pipeline flowing left to right. + +```shell +ls | where type == "dir" | table +# => ╭────┬──────────┬──────┬─────────┬───────────────╮ +# => │ # │ name │ type │ size │ modified │ +# => ├────┼──────────┼──────┼─────────┼───────────────┤ +# => │ 0 │ .cargo │ dir │ 0 B │ 9 minutes ago │ +# => │ 1 │ assets │ dir │ 0 B │ 2 weeks ago │ +# => │ 2 │ crates │ dir │ 4.0 KiB │ 2 weeks ago │ +# => │ 3 │ docker │ dir │ 0 B │ 2 weeks ago │ +# => │ 4 │ docs │ dir │ 0 B │ 2 weeks ago │ +# => │ 5 │ images │ dir │ 0 B │ 2 weeks ago │ +# => │ 6 │ pkg_mgrs │ dir │ 0 B │ 2 weeks ago │ +# => │ 7 │ samples │ dir │ 0 B │ 2 weeks ago │ +# => │ 8 │ src │ dir │ 4.0 KiB │ 2 weeks ago │ +# => │ 9 │ target │ dir │ 0 B │ a day ago │ +# => │ 10 │ tests │ dir │ 4.0 KiB │ 2 weeks ago │ +# => │ 11 │ wix │ dir │ 0 B │ 2 weeks ago │ +# => ╰────┴──────────┴──────┴─────────┴───────────────╯ +``` + +Because most of the time you'll want to see the output of a pipeline, `table` is assumed. +We could have also written the above: + +```shell +ls | where type == "dir" +``` + +Being able to use the same commands and compose them differently is an important philosophy in Nu. +For example, we could use the built-in `ps` command to get a list of the running processes, using the same `where` as above. + +```shell +ps | where cpu > 0 +# => ╭───┬───────┬───────────┬───────┬───────────┬───────────╮ +# => │ # │ pid │ name │ cpu │ mem │ virtual │ +# => ├───┼───────┼───────────┼───────┼───────────┼───────────┤ +# => │ 0 │ 2240 │ Slack.exe │ 16.40 │ 178.3 MiB │ 232.6 MiB │ +# => │ 1 │ 16948 │ Slack.exe │ 16.32 │ 205.0 MiB │ 197.9 MiB │ +# => │ 2 │ 17700 │ nu.exe │ 3.77 │ 26.1 MiB │ 8.8 MiB │ +# => ╰───┴───────┴───────────┴───────┴───────────┴───────────╯ +``` + +### Opening files + +Nu can load file and URL contents as raw text or structured data (if it recognizes the format). +For example, you can load a .toml file as structured data and explore it: + +```shell +open Cargo.toml +# => ╭──────────────────┬────────────────────╮ +# => │ bin │ [table 1 row] │ +# => │ dependencies │ {record 25 fields} │ +# => │ dev-dependencies │ {record 8 fields} │ +# => │ features │ {record 10 fields} │ +# => │ package │ {record 13 fields} │ +# => │ patch │ {record 1 field} │ +# => │ profile │ {record 3 fields} │ +# => │ target │ {record 3 fields} │ +# => │ workspace │ {record 1 field} │ +# => ╰──────────────────┴────────────────────╯ +``` + +We can pipe this into a command that gets the contents of one of the columns: + +```shell +open Cargo.toml | get package +# => ╭───────────────┬────────────────────────────────────╮ +# => │ authors │ [list 1 item] │ +# => │ default-run │ nu │ +# => │ description │ A new type of shell │ +# => │ documentation │ https://www.nushell.sh/book/ │ +# => │ edition │ 2018 │ +# => │ exclude │ [list 1 item] │ +# => │ homepage │ https://www.nushell.sh │ +# => │ license │ MIT │ +# => │ metadata │ {record 1 field} │ +# => │ name │ nu │ +# => │ repository │ https://github.com/nushell/nushell │ +# => │ rust-version │ 1.60 │ +# => │ version │ 0.72.0 │ +# => ╰───────────────┴────────────────────────────────────╯ +``` + +And if needed we can drill down further: + +```shell +open Cargo.toml | get package.version +# => 0.72.0 +``` + +### Plugins + +Nu supports plugins that offer additional functionality to the shell and follow the same structured data model that built-in commands use. There are a few examples in the `crates/nu_plugins_*` directories. + +Plugins are binaries that are available in your path and follow a `nu_plugin_*` naming convention. +These binaries interact with nu via a simple JSON-RPC protocol where the command identifies itself and passes along its configuration, making it available for use. +If the plugin is a filter, data streams to it one element at a time, and it can stream data back in return via stdin/stdout. +If the plugin is a sink, it is given the full vector of final data and is given free reign over stdin/stdout to use as it pleases. + +The [awesome-nu repo](https://github.com/nushell/awesome-nu#plugins) lists a variety of nu-plugins while the [showcase repo](https://github.com/nushell/showcase) *shows* off informative blog posts that have been written about Nushell along with videos that highlight technical +topics that have been presented. + +## Goals + +Nu adheres closely to a set of goals that make up its design philosophy. As features are added, they are checked against these goals. + +- First and foremost, Nu is cross-platform. Commands and techniques should work across platforms and Nu has [first-class support for Windows, macOS, and Linux](devdocs/PLATFORM_SUPPORT.md). + +- Nu ensures compatibility with existing platform-specific executables. + +- Nu's workflow and tools should have the usability expected of modern software in 2022 (and beyond). + +- Nu views data as either structured or unstructured. It is a structured shell like PowerShell. + +- Finally, Nu views data functionally. Rather than using mutation, pipelines act as a means to load, change, and save data without mutable state. + +## Officially Supported By + +Please submit an issue or PR to be added to this list. + +- [zoxide](https://github.com/ajeetdsouza/zoxide) +- [starship](https://github.com/starship/starship) +- [oh-my-posh](https://ohmyposh.dev) +- [Couchbase Shell](https://couchbase.sh) +- [virtualenv](https://github.com/pypa/virtualenv) +- [atuin](https://github.com/ellie/atuin) +- [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell) +- [Dorothy](http://github.com/bevry/dorothy) +- [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell) +- [x-cmd](https://x-cmd.com/mod/nu) +- [vfox](https://github.com/version-fox/vfox) + +## Contributing + +See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed! + + + + + +## License + +The project is made available under the MIT license. See the `LICENSE` file for more information. diff --git a/nushell/SECURITY.md b/nushell/SECURITY.md new file mode 100644 index 0000000..3f161a7 --- /dev/null +++ b/nushell/SECURITY.md @@ -0,0 +1,29 @@ +# 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. diff --git a/nushell/assets/icons/nushell-original.png b/nushell/assets/icons/nushell-original.png new file mode 100644 index 0000000..14501a3 Binary files /dev/null and b/nushell/assets/icons/nushell-original.png differ diff --git a/nushell/assets/nu_logo.ico b/nushell/assets/nu_logo.ico new file mode 100644 index 0000000..b74f2a3 Binary files /dev/null and b/nushell/assets/nu_logo.ico differ diff --git a/nushell/assets/nushell-autocomplete6.gif b/nushell/assets/nushell-autocomplete6.gif new file mode 100644 index 0000000..d5afd9d Binary files /dev/null and b/nushell/assets/nushell-autocomplete6.gif differ diff --git a/nushell/benches/README.md b/nushell/benches/README.md new file mode 100644 index 0000000..93a13af --- /dev/null +++ b/nushell/benches/README.md @@ -0,0 +1,7 @@ +# Divan benchmarks + +These are benchmarks using [Divan](https://github.com/nvzqz/divan), a microbenchmarking tool for Rust. + +Run all benchmarks with `cargo bench` + +Or run individual benchmarks like `cargo bench -- ` e.g. `cargo bench -- parse` \ No newline at end of file diff --git a/nushell/benches/benchmarks.rs b/nushell/benches/benchmarks.rs new file mode 100644 index 0000000..a6fd2cf --- /dev/null +++ b/nushell/benches/benchmarks.rs @@ -0,0 +1,541 @@ +use nu_cli::{eval_source, evaluate_commands}; +use nu_plugin_core::{Encoder, EncodingType}; +use nu_plugin_protocol::{PluginCallResponse, PluginOutput}; +use nu_protocol::{ + PipelineData, Signals, Span, Spanned, Value, + engine::{EngineState, Stack}, +}; +use nu_std::load_standard_library; +use nu_utils::{get_default_config, get_default_env}; +use std::{ + fmt::Write, + hint::black_box, + rc::Rc, + sync::{Arc, atomic::AtomicBool}, +}; +use tango_bench::{IntoBenchmarks, benchmark_fn, tango_benchmarks, tango_main}; + +fn load_bench_commands() -> EngineState { + nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()) +} + +fn setup_engine() -> EngineState { + let mut engine_state = load_bench_commands(); + let cwd = std::env::current_dir() + .unwrap() + .into_os_string() + .into_string() + .unwrap(); + + // 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.generate_nu_constant(); + + engine_state +} + +fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) { + let mut engine = setup_engine(); + let commands = Spanned { + span: Span::unknown(), + item: command.to_string(), + }; + + let mut stack = Stack::new(); + + evaluate_commands( + &commands, + &mut engine, + &mut stack, + PipelineData::empty(), + Default::default(), + ) + .unwrap(); + + (stack, engine) +} + +// generate a new table data with `row_cnt` rows, `col_cnt` columns. +fn encoding_test_data(row_cnt: usize, col_cnt: usize) -> Value { + let record = Value::test_record( + (0..col_cnt) + .map(|x| (format!("col_{x}"), Value::test_int(x as i64))) + .collect(), + ); + + Value::list(vec![record; row_cnt], Span::test_data()) +} + +fn bench_command( + name: impl Into, + command: impl Into + Clone, + stack: Stack, + engine: EngineState, +) -> impl IntoBenchmarks { + let commands = Spanned { + span: Span::unknown(), + item: command.into(), + }; + [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( + name: &str, + fname: String, + source: Vec, + stack: Stack, + engine: EngineState, +) -> impl IntoBenchmarks { + [benchmark_fn(name, move |b| { + let stack = stack.clone(); + let engine = engine.clone(); + 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 { + 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 +} + +fn create_example_table_nrows(n: usize) -> 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 +} + +fn bench_record_create(n: usize) -> impl IntoBenchmarks { + bench_command( + format!("record_create_{n}"), + create_flat_record_string(n), + Stack::new(), + setup_engine(), + ) +} + +fn bench_record_flat_access(n: usize) -> impl IntoBenchmarks { + let setup_command = create_flat_record_string(n); + 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()); + binary_data.set_position(0); + let _: Result, _> = + black_box(encoder.decode(&mut binary_data)); + }) + }, + )] +} + +fn decode_msgpack(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"msgpack").unwrap(); + let mut res = vec![]; + encoder.encode(&test_data, &mut res).unwrap(); + + [benchmark_fn( + format!("decode_msgpack_{}_{}", row_cnt, col_cnt), + move |b| { + let res = res.clone(); + b.iter(move || { + let mut binary_data = std::io::Cursor::new(res.clone()); + binary_data.set_position(0); + let _: Result, _> = + black_box(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!(); diff --git a/nushell/clippy/wasm/clippy.toml b/nushell/clippy/wasm/clippy.toml new file mode 100644 index 0000000..d1092af --- /dev/null +++ b/nushell/clippy/wasm/clippy.toml @@ -0,0 +1,3 @@ +[[disallowed-types]] +path = "std::time::Instant" +reason = "WASM panics if used, use `web_time::Instant` instead" diff --git a/nushell/crates/README.md b/nushell/crates/README.md new file mode 100644 index 0000000..ca1e44b --- /dev/null +++ b/nushell/crates/README.md @@ -0,0 +1,13 @@ +# Nushell core libraries and plugins + +These sub-crates form both the foundation for Nu and a set of plugins which extend Nu with additional functionality. + +Foundational libraries are split into two kinds of crates: + +* Core crates - those crates that work together to build the Nushell language engine +* Support crates - a set of crates that support the engine with additional features like JSON support, ANSI support, and more. + +Plugins are likewise also split into two types: + +* Core plugins - plugins that provide part of the default experience of Nu, including access to the system properties, processes, and web-connectivity features. +* Extra plugins - these plugins run a wide range of different capabilities like working with different file types, charting, viewing binary data, and more. diff --git a/nushell/crates/nu-cli/Cargo.toml b/nushell/crates/nu-cli/Cargo.toml new file mode 100644 index 0000000..a6c5d9d --- /dev/null +++ b/nushell/crates/nu-cli/Cargo.toml @@ -0,0 +1,54 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "CLI-related functionality for Nushell" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli" +edition = "2024" +license = "MIT" +name = "nu-cli" +version = "0.105.2" + +[lib] +bench = false + +[dev-dependencies] +nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.105.2" } +nu-command = { path = "../nu-command", version = "0.105.2" } +nu-std = { path = "../nu-std", version = "0.105.2" } +nu-test-support = { path = "../nu-test-support", version = "0.105.2" } +rstest = { workspace = true, default-features = false } +tempfile = { workspace = true } + +[dependencies] +nu-cmd-base = { path = "../nu-cmd-base", version = "0.105.2" } +nu-engine = { path = "../nu-engine", version = "0.105.2", features = ["os"] } +nu-glob = { path = "../nu-glob", version = "0.105.2" } +nu-path = { path = "../nu-path", version = "0.105.2" } +nu-parser = { path = "../nu-parser", version = "0.105.2" } +nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.105.2", optional = true } +nu-protocol = { path = "../nu-protocol", version = "0.105.2", features = ["os"] } +nu-utils = { path = "../nu-utils", version = "0.105.2" } +nu-color-config = { path = "../nu-color-config", version = "0.105.2" } +nu-ansi-term = { workspace = true } +reedline = { workspace = true, features = ["bashisms", "sqlite"] } + +chrono = { default-features = false, features = ["std"], workspace = true } +crossterm = { workspace = true } +fancy-regex = { workspace = true } +is_executable = { workspace = true } +log = { workspace = true } +lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] } +miette = { workspace = true, features = ["fancy-no-backtrace"] } +nucleo-matcher = { workspace = true } +percent-encoding = { workspace = true } +sysinfo = { workspace = true } +strum = { workspace = true } +unicode-segmentation = { workspace = true } +uuid = { workspace = true, features = ["v4"] } +which = { workspace = true } + +[features] +plugin = ["nu-plugin-engine"] +system-clipboard = ["reedline/system_clipboard"] + +[lints] +workspace = true diff --git a/nushell/crates/nu-cli/LICENSE b/nushell/crates/nu-cli/LICENSE new file mode 100644 index 0000000..ae174e8 --- /dev/null +++ b/nushell/crates/nu-cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nushell/crates/nu-cli/README.md b/nushell/crates/nu-cli/README.md new file mode 100644 index 0000000..e0eca3c --- /dev/null +++ b/nushell/crates/nu-cli/README.md @@ -0,0 +1,7 @@ +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. diff --git a/nushell/crates/nu-cli/src/commands/commandline/commandline_.rs b/nushell/crates/nu-cli/src/commands/commandline/commandline_.rs new file mode 100644 index 0000000..909c2cc --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/commandline/commandline_.rs @@ -0,0 +1,35 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct Commandline; + +impl Command for Commandline { + fn name(&self) -> &str { + "commandline" + } + + fn signature(&self) -> Signature { + Signature::build("commandline") + .input_output_types(vec![(Type::Nothing, Type::String)]) + .category(Category::Core) + } + + fn description(&self) -> &str { + "View the current command line input buffer." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["repl", "interactive"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let repl = engine_state.repl_state.lock().expect("repl state mutex"); + Ok(Value::string(repl.buffer.clone(), call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cli/src/commands/commandline/edit.rs b/nushell/crates/nu-cli/src/commands/commandline/edit.rs new file mode 100644 index 0000000..7b51ba0 --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/commandline/edit.rs @@ -0,0 +1,66 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct CommandlineEdit; + +impl Command for CommandlineEdit { + fn name(&self) -> &str { + "commandline edit" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .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'), + ) + .required( + "str", + SyntaxShape::String, + "The string to perform the operation with.", + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Modify the current command line input buffer." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["repl", "interactive"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let str: String = call.req(engine_state, stack, 0)?; + let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); + if call.has_flag(engine_state, stack, "append")? { + repl.buffer.push_str(&str); + } else if call.has_flag(engine_state, stack, "insert")? { + let cursor_pos = repl.cursor_pos; + repl.buffer.insert_str(cursor_pos, &str); + repl.cursor_pos += str.len(); + } else { + repl.buffer = str; + repl.cursor_pos = repl.buffer.len(); + } + Ok(Value::nothing(call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cli/src/commands/commandline/get_cursor.rs b/nushell/crates/nu-cli/src/commands/commandline/get_cursor.rs new file mode 100644 index 0000000..61050f6 --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/commandline/get_cursor.rs @@ -0,0 +1,52 @@ +use nu_engine::command_prelude::*; +use unicode_segmentation::UnicodeSegmentation; + +#[derive(Clone)] +pub struct CommandlineGetCursor; + +impl Command for CommandlineGetCursor { + fn name(&self) -> &str { + "commandline get-cursor" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_types(vec![(Type::Nothing, Type::Int)]) + .allow_variants_without_examples(true) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Get the current cursor position." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["repl", "interactive"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let repl = engine_state.repl_state.lock().expect("repl state mutex"); + 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"); + match i64::try_from(char_pos) { + Ok(pos) => Ok(Value::int(pos, call.head).into_pipeline_data()), + Err(e) => Err(ShellError::GenericError { + error: "Failed to convert cursor position to int".to_string(), + msg: e.to_string(), + span: None, + help: None, + inner: vec![], + }), + } + } +} diff --git a/nushell/crates/nu-cli/src/commands/commandline/mod.rs b/nushell/crates/nu-cli/src/commands/commandline/mod.rs new file mode 100644 index 0000000..bb4c9b0 --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/commandline/mod.rs @@ -0,0 +1,9 @@ +mod commandline_; +mod edit; +mod get_cursor; +mod set_cursor; + +pub use commandline_::Commandline; +pub use edit::CommandlineEdit; +pub use get_cursor::CommandlineGetCursor; +pub use set_cursor::CommandlineSetCursor; diff --git a/nushell/crates/nu-cli/src/commands/commandline/set_cursor.rs b/nushell/crates/nu-cli/src/commands/commandline/set_cursor.rs new file mode 100644 index 0000000..012bd8c --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/commandline/set_cursor.rs @@ -0,0 +1,65 @@ +use nu_engine::command_prelude::*; + +use unicode_segmentation::UnicodeSegmentation; + +#[derive(Clone)] +pub struct CommandlineSetCursor; + +impl Command for CommandlineSetCursor { + fn name(&self) -> &str { + "commandline set-cursor" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .switch( + "end", + "set the current cursor position to the end of the buffer", + Some('e'), + ) + .optional("pos", SyntaxShape::Int, "Cursor position to be set.") + .category(Category::Core) + } + + fn description(&self) -> &str { + "Set the current cursor position." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["repl", "interactive"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); + if let Some(pos) = call.opt::(engine_state, stack, 0)? { + repl.cursor_pos = if pos <= 0 { + 0usize + } else { + repl.buffer + .grapheme_indices(true) + .map(|(i, _c)| i) + .nth(pos as usize) + .unwrap_or(repl.buffer.len()) + }; + Ok(Value::nothing(call.head).into_pipeline_data()) + } else if call.has_flag(engine_state, stack, "end")? { + repl.cursor_pos = repl.buffer.len(); + Ok(Value::nothing(call.head).into_pipeline_data()) + } else { + Err(ShellError::GenericError { + error: "Required a positional argument or a flag".to_string(), + msg: "".to_string(), + span: None, + help: None, + inner: vec![], + }) + } + } +} diff --git a/nushell/crates/nu-cli/src/commands/default_context.rs b/nushell/crates/nu-cli/src/commands/default_context.rs new file mode 100644 index 0000000..ad19f18 --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/default_context.rs @@ -0,0 +1,36 @@ +use crate::commands::*; +use nu_protocol::engine::{EngineState, StateWorkingSet}; + +pub fn add_cli_context(mut engine_state: EngineState) -> EngineState { + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + + macro_rules! bind_command { + ( $( $command:expr ),* $(,)? ) => { + $( working_set.add_decl(Box::new($command)); )* + }; + } + + bind_command! { + Commandline, + CommandlineEdit, + CommandlineGetCursor, + CommandlineSetCursor, + History, + HistoryImport, + HistorySession, + Keybindings, + KeybindingsDefault, + KeybindingsList, + KeybindingsListen, + }; + + working_set.render() + }; + + if let Err(err) = engine_state.merge_delta(delta) { + eprintln!("Error creating CLI command context: {err:?}"); + } + + engine_state +} diff --git a/nushell/crates/nu-cli/src/commands/history/fields.rs b/nushell/crates/nu-cli/src/commands/history/fields.rs new file mode 100644 index 0000000..a5b4422 --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/history/fields.rs @@ -0,0 +1,9 @@ +// Each const is named after a HistoryItem field, and the value is the field name to be displayed to +// the user (or accept during import). +pub const COMMAND_LINE: &str = "command"; +pub const START_TIMESTAMP: &str = "start_timestamp"; +pub const HOSTNAME: &str = "hostname"; +pub const CWD: &str = "cwd"; +pub const EXIT_STATUS: &str = "exit_status"; +pub const DURATION: &str = "duration"; +pub const SESSION_ID: &str = "session_id"; diff --git a/nushell/crates/nu-cli/src/commands/history/history_.rs b/nushell/crates/nu-cli/src/commands/history/history_.rs new file mode 100644 index 0000000..10f0a21 --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/history/history_.rs @@ -0,0 +1,206 @@ +use nu_engine::command_prelude::*; +use nu_protocol::{ + HistoryFileFormat, + shell_error::{self, io::IoError}, +}; +use reedline::{ + FileBackedHistory, History as ReedlineHistory, HistoryItem, SearchDirection, SearchQuery, + SqliteBackedHistory, +}; + +use super::fields; + +#[derive(Clone)] +pub struct History; + +impl Command for History { + fn name(&self) -> &str { + "history" + } + + fn description(&self) -> &str { + "Get the command history." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("history") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .allow_variants_without_examples(true) + .switch("clear", "Clears out the history entries", Some('c')) + .switch( + "long", + "Show long listing of entries for sqlite history", + Some('l'), + ) + .category(Category::History) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + + let Some(history) = engine_state.history_config() else { + return Ok(PipelineData::empty()); + }; + // 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 { + return Err(ShellError::ConfigDirNotFound { span: Some(head) }); + }; + + if call.has_flag(engine_state, stack, "clear")? { + let _ = std::fs::remove_file(history_path); + // TODO: FIXME also clear the auxiliary files when using sqlite + return Ok(PipelineData::empty()); + } + + let long = call.has_flag(engine_state, stack, "long")?; + let signals = engine_state.signals().clone(); + let history_reader: Option> = match history.file_format { + HistoryFileFormat::Sqlite => { + SqliteBackedHistory::with_file(history_path.clone(), None, None) + .map(|inner| { + let boxed: Box = Box::new(inner); + boxed + }) + .ok() + } + HistoryFileFormat::Plaintext => { + FileBackedHistory::with_file(history.max_size as usize, history_path.clone()) + .map(|inner| { + let boxed: Box = 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( + shell_error::io::ErrorKind::FileNotFound, + 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( + shell_error::io::ErrorKind::FileNotFound, + head, + history_path, + ))? + .into_pipeline_data(head, signals)), + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "history | length", + description: "Get current history length", + result: None, + }, + Example { + example: "history | last 5", + description: "Show last 5 commands you have ran", + result: None, + }, + Example { + example: "history | where command =~ cargo | get command", + description: "Search all the commands from history that contains 'cargo'", + result: None, + }, + ] + } +} + +fn create_sqlite_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) -> Value { + //1. Format all the values + //2. Create a record of either short or long columns and values + + let item_id_value = Value::int( + entry + .id + .and_then(|id| id.to_string().parse::().ok()) + .unwrap_or_default(), + head, + ); + let start_timestamp_value = Value::date( + entry.start_timestamp.unwrap_or_default().fixed_offset(), + head, + ); + let command_value = Value::string(entry.command_line, head); + let session_id_value = Value::int( + entry + .session_id + .and_then(|id| id.to_string().parse::().ok()) + .unwrap_or_default(), + 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( + entry + .duration + .and_then(|d| d.as_nanos().try_into().ok()) + .unwrap_or(0), + head, + ); + let exit_status_value = Value::int(entry.exit_status.unwrap_or(0), head); + let index_value = Value::int(idx as i64, head); + if long { + Value::record( + record! { + "item_id" => item_id_value, + fields::START_TIMESTAMP => start_timestamp_value, + fields::COMMAND_LINE => command_value, + fields::SESSION_ID => session_id_value, + fields::HOSTNAME => hostname_value, + fields::CWD => cwd_value, + fields::DURATION => duration_value, + fields::EXIT_STATUS => exit_status_value, + "idx" => index_value, + }, + head, + ) + } else { + Value::record( + record! { + fields::START_TIMESTAMP => start_timestamp_value, + fields::COMMAND_LINE => command_value, + fields::CWD => cwd_value, + fields::DURATION => duration_value, + fields::EXIT_STATUS => exit_status_value, + }, + head, + ) + } +} diff --git a/nushell/crates/nu-cli/src/commands/history/history_import.rs b/nushell/crates/nu-cli/src/commands/history/history_import.rs new file mode 100644 index 0000000..360273a --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/history/history_import.rs @@ -0,0 +1,440 @@ +use std::path::{Path, PathBuf}; + +use nu_engine::command_prelude::*; +use nu_protocol::{ + HistoryFileFormat, + shell_error::{self, io::IoError}, +}; + +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, 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 { + 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 { + let span = call.head; + let ok = Ok(Value::nothing(call.head).into_pipeline_data()); + + let Some(history) = engine_state.history_config() else { + return ok; + }; + let Some(current_history_path) = history.file_path() else { + return Err(ShellError::ConfigDirNotFound { span: span.into() }); + }; + if let Some(bak_path) = backup(¤t_history_path, span)? { + println!("Backed history to {}", bak_path.display()); + } + match input { + PipelineData::Empty => { + let other_format = match history.file_format { + HistoryFileFormat::Sqlite => HistoryFileFormat::Plaintext, + HistoryFileFormat::Plaintext => HistoryFileFormat::Sqlite, + }; + let src = new_backend(other_format, None)?; + let mut dst = new_backend(history.file_format, Some(current_history_path))?; + let items = src + .search(SearchQuery::everything( + reedline::SearchDirection::Forward, + None, + )) + .map_err(error_from_reedline)? + .into_iter() + .map(Ok); + import(dst.as_mut(), items) + } + _ => { + let input = input.into_iter().map(item_from_value); + import( + new_backend(history.file_format, Some(current_history_path))?.as_mut(), + input, + ) + } + }?; + + ok + } +} + +fn new_backend( + format: HistoryFileFormat, + path: Option, +) -> Result, 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, + ) -> Result, ShellError> { + result + .map(|x| Box::new(x) as Box) + .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>, +) -> 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 { + 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 { + 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( + rec: &mut Record, + field: &'static str, + f: impl FnOnce(Value) -> Result, + ) -> Result, 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::>(); + return Err(ShellError::TypeMismatch { + err_message: format!("unsupported column names: {}", cols.join(", ")), + span, + }); + } + Ok(item) +} + +fn duration_from_value(v: Value, span: Span) -> Result { + chrono::Duration::nanoseconds(v.as_duration()?) + .to_std() + .map_err(|_| ShellError::NeedsPositiveValue { span }) +} + +fn find_backup_path(path: &Path, span: Span) -> Result { + 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, 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, + "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.not_found_as(NotFound::File), + "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()); + } +} diff --git a/nushell/crates/nu-cli/src/commands/history/history_session.rs b/nushell/crates/nu-cli/src/commands/history/history_session.rs new file mode 100644 index 0000000..d6e0d0d --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/history/history_session.rs @@ -0,0 +1,38 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct HistorySession; + +impl Command for HistorySession { + fn name(&self) -> &str { + "history session" + } + + fn description(&self) -> &str { + "Get the command history session." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("history session") + .category(Category::History) + .input_output_types(vec![(Type::Nothing, Type::Int)]) + } + + fn examples(&self) -> Vec { + vec![Example { + example: "history session", + description: "Get current history session", + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::int(engine_state.history_session_id, call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cli/src/commands/history/mod.rs b/nushell/crates/nu-cli/src/commands/history/mod.rs new file mode 100644 index 0000000..c36b560 --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/history/mod.rs @@ -0,0 +1,8 @@ +mod fields; +mod history_; +mod history_import; +mod history_session; + +pub use history_::History; +pub use history_import::HistoryImport; +pub use history_session::HistorySession; diff --git a/nushell/crates/nu-cli/src/commands/keybindings.rs b/nushell/crates/nu-cli/src/commands/keybindings.rs new file mode 100644 index 0000000..34ae0fa --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/keybindings.rs @@ -0,0 +1,41 @@ +use nu_engine::{command_prelude::*, get_full_help}; + +#[derive(Clone)] +pub struct Keybindings; + +impl Command for Keybindings { + fn name(&self) -> &str { + "keybindings" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .category(Category::Platform) + .input_output_types(vec![(Type::Nothing, Type::String)]) + } + + fn description(&self) -> &str { + "Keybindings related commands." + } + + fn extra_description(&self) -> &str { + 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: + https://www.nushell.sh/book/line_editor.html"# + } + + fn search_terms(&self) -> Vec<&str> { + vec!["shortcut", "hotkey"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cli/src/commands/keybindings_default.rs b/nushell/crates/nu-cli/src/commands/keybindings_default.rs new file mode 100644 index 0000000..c5ca29d --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/keybindings_default.rs @@ -0,0 +1,54 @@ +use nu_engine::command_prelude::*; +use reedline::get_reedline_default_keybindings; + +#[derive(Clone)] +pub struct KeybindingsDefault; + +impl Command for KeybindingsDefault { + fn name(&self) -> &str { + "keybindings default" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .category(Category::Platform) + .input_output_types(vec![(Type::Nothing, Type::table())]) + } + + fn description(&self) -> &str { + "List default keybindings." + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get list with default keybindings", + example: "keybindings default", + result: None, + }] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let records = get_reedline_default_keybindings() + .into_iter() + .map(|(mode, modifier, code, event)| { + Value::record( + record! { + "mode" => Value::string(mode, call.head), + "modifier" => Value::string(modifier, call.head), + "code" => Value::string(code, call.head), + "event" => Value::string(event, call.head), + }, + call.head, + ) + }) + .collect(); + + Ok(Value::list(records, call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cli/src/commands/keybindings_list.rs b/nushell/crates/nu-cli/src/commands/keybindings_list.rs new file mode 100644 index 0000000..6332ec3 --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/keybindings_list.rs @@ -0,0 +1,117 @@ +use nu_engine::command_prelude::*; +use reedline::{ + get_reedline_edit_commands, get_reedline_keybinding_modifiers, get_reedline_keycodes, + get_reedline_prompt_edit_modes, get_reedline_reedline_events, +}; + +#[derive(Clone)] +pub struct KeybindingsList; + +impl Command for KeybindingsList { + fn name(&self) -> &str { + "keybindings list" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_types(vec![(Type::Nothing, Type::table())]) + .switch("modifiers", "list of modifiers", Some('m')) + .switch("keycodes", "list of keycodes", Some('k')) + .switch("modes", "list of edit modes", Some('o')) + .switch("events", "list of reedline event", Some('e')) + .switch("edits", "list of edit commands", Some('d')) + .category(Category::Platform) + } + + fn description(&self) -> &str { + "List available options that can be used to create keybindings." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get list of key modifiers", + example: "keybindings list --modifiers", + result: None, + }, + Example { + description: "Get list of reedline events and edit commands", + example: "keybindings list -e -d", + result: None, + }, + Example { + description: "Get list with all the available options", + example: "keybindings list", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let all_options = ["modifiers", "keycodes", "edits", "modes", "events"]; + + let presence = all_options + .iter() + .map(|option| call.has_flag(engine_state, stack, option)) + .collect::, ShellError>>()?; + + let no_option_specified = presence.iter().all(|present| !*present); + + let records = all_options + .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()) + } +} + +fn get_records(entry_type: &str, span: Span) -> Vec { + let values = match entry_type { + "modifiers" => get_reedline_keybinding_modifiers().sorted(), + "keycodes" => get_reedline_keycodes().sorted(), + "edits" => get_reedline_edit_commands().sorted(), + "modes" => get_reedline_prompt_edit_modes().sorted(), + "events" => get_reedline_reedline_events().sorted(), + _ => Vec::new(), + }; + + values + .iter() + .map(|edit| edit.split('\n')) + .flat_map(|edit| edit.map(|edit| convert_to_record(edit, entry_type, span))) + .collect() +} + +fn convert_to_record(edit: &str, entry_type: &str, span: Span) -> Value { + Value::record( + record! { + "type" => Value::string(entry_type, span), + "name" => Value::string(edit, span), + }, + span, + ) +} + +// Helper to sort a vec and return a vec +trait SortedImpl { + fn sorted(self) -> Self; +} + +impl SortedImpl for Vec +where + E: std::cmp::Ord, +{ + fn sorted(mut self) -> Self { + self.sort(); + self + } +} diff --git a/nushell/crates/nu-cli/src/commands/keybindings_listen.rs b/nushell/crates/nu-cli/src/commands/keybindings_listen.rs new file mode 100644 index 0000000..2232b9d --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/keybindings_listen.rs @@ -0,0 +1,200 @@ +use crossterm::{ + QueueableCommand, event::Event, event::KeyCode, event::KeyEvent, execute, terminal, +}; +use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; +use std::io::{Write, stdout}; + +#[derive(Clone)] +pub struct KeybindingsListen; + +impl Command for KeybindingsListen { + fn name(&self) -> &str { + "keybindings listen" + } + + fn description(&self) -> &str { + "Get input from the user." + } + + fn extra_description(&self) -> &str { + "This is an internal debugging tool. For better output, try `input listen --types [key]`" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .category(Category::Platform) + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + println!("Type any key combination to see key details. Press ESC to abort."); + + match print_events(engine_state) { + Ok(v) => Ok(v.into_pipeline_data()), + Err(e) => { + terminal::disable_raw_mode().map_err(|err| { + IoError::new_internal( + err, + "Could not disable raw mode", + nu_protocol::location!(), + ) + })?; + Err(ShellError::GenericError { + error: "Error with input".into(), + msg: "".into(), + span: None, + help: Some(e.to_string()), + inner: vec![], + }) + } + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Type and see key event codes", + example: "keybindings listen", + result: None, + }] + } +} + +pub fn print_events(engine_state: &EngineState) -> Result { + let config = engine_state.get_config(); + + stdout().flush().map_err(|err| { + IoError::new_internal(err, "Could not flush stdout", nu_protocol::location!()) + })?; + terminal::enable_raw_mode().map_err(|err| { + IoError::new_internal(err, "Could not enable raw mode", nu_protocol::location!()) + })?; + + if config.use_kitty_protocol { + if let Ok(false) = crossterm::terminal::supports_keyboard_enhancement() { + println!("WARN: The terminal doesn't support use_kitty_protocol config.\r"); + } + + // enable kitty protocol + // + // Note that, currently, only the following support this protocol: + // * [kitty terminal](https://sw.kovidgoyal.net/kitty/) + // * [foot terminal](https://codeberg.org/dnkl/foot/issues/319) + // * [WezTerm terminal](https://wezfurlong.org/wezterm/config/lua/config/enable_kitty_keyboard.html) + // * [notcurses library](https://github.com/dankamongmen/notcurses/issues/2131) + // * [neovim text editor](https://github.com/neovim/neovim/pull/18181) + // * [kakoune text editor](https://github.com/mawww/kakoune/issues/4103) + // * [dte text editor](https://gitlab.com/craigbarnes/dte/-/issues/138) + // + // Refer to https://sw.kovidgoyal.net/kitty/keyboard-protocol/ if you're curious. + let _ = execute!( + stdout(), + crossterm::event::PushKeyboardEnhancementFlags( + crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + ) + ); + } + + let mut stdout = std::io::BufWriter::new(std::io::stderr()); + + loop { + let event = crossterm::event::read().map_err(|err| { + IoError::new_internal(err, "Could not read event", nu_protocol::location!()) + })?; + if event == Event::Key(KeyCode::Esc.into()) { + break; + } + // stdout.queue(crossterm::style::Print(format!("event: {:?}", &event)))?; + // stdout.queue(crossterm::style::Print("\r\n"))?; + + // Get a record + let v = print_events_helper(event)?; + // Print out the record + let o = match v { + Value::Record { val, .. } => val + .iter() + .map(|(x, y)| format!("{}: {}", x, y.to_expanded_string("", config))) + .collect::>() + .join(", "), + + _ => "".to_string(), + }; + stdout.queue(crossterm::style::Print(o)).map_err(|err| { + IoError::new_internal( + err, + "Could not print output record", + nu_protocol::location!(), + ) + })?; + stdout + .queue(crossterm::style::Print("\r\n")) + .map_err(|err| { + IoError::new_internal(err, "Could not print linebreak", nu_protocol::location!()) + })?; + stdout.flush().map_err(|err| { + IoError::new_internal(err, "Could not flush", nu_protocol::location!()) + })?; + } + + if config.use_kitty_protocol { + let _ = execute!( + std::io::stdout(), + crossterm::event::PopKeyboardEnhancementFlags + ); + } + + terminal::disable_raw_mode().map_err(|err| { + IoError::new_internal(err, "Could not disable raw mode", nu_protocol::location!()) + })?; + + Ok(Value::nothing(Span::unknown())) +} + +// this fn is totally ripped off from crossterm's examples +// it's really a diagnostic routine to see if crossterm is +// even seeing the events. if you press a key and no events +// are printed, it's a good chance your terminal is eating +// those events. +fn print_events_helper(event: Event) -> Result { + if let Event::Key(KeyEvent { + code, + modifiers, + kind, + state, + }) = event + { + match code { + KeyCode::Char(c) => { + let record = record! { + "char" => Value::string(format!("{c}"), Span::unknown()), + "code" => Value::string(format!("{:#08x}", u32::from(c)), Span::unknown()), + "modifier" => Value::string(format!("{modifiers:?}"), Span::unknown()), + "flags" => Value::string(format!("{modifiers:#08b}"), Span::unknown()), + "kind" => Value::string(format!("{kind:?}"), Span::unknown()), + "state" => Value::string(format!("{state:?}"), Span::unknown()), + }; + Ok(Value::record(record, Span::unknown())) + } + _ => { + let record = record! { + "code" => Value::string(format!("{code:?}"), Span::unknown()), + "modifier" => Value::string(format!("{modifiers:?}"), Span::unknown()), + "flags" => Value::string(format!("{modifiers:#08b}"), Span::unknown()), + "kind" => Value::string(format!("{kind:?}"), Span::unknown()), + "state" => Value::string(format!("{state:?}"), Span::unknown()), + }; + Ok(Value::record(record, Span::unknown())) + } + } + } else { + let record = record! { "event" => Value::string(format!("{event:?}"), Span::unknown()) }; + Ok(Value::record(record, Span::unknown())) + } +} diff --git a/nushell/crates/nu-cli/src/commands/mod.rs b/nushell/crates/nu-cli/src/commands/mod.rs new file mode 100644 index 0000000..4a9dd9e --- /dev/null +++ b/nushell/crates/nu-cli/src/commands/mod.rs @@ -0,0 +1,16 @@ +mod commandline; +mod default_context; +mod history; +mod keybindings; +mod keybindings_default; +mod keybindings_list; +mod keybindings_listen; + +pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor}; +pub use history::{History, HistoryImport, HistorySession}; +pub use keybindings::Keybindings; +pub use keybindings_default::KeybindingsDefault; +pub use keybindings_list::KeybindingsList; +pub use keybindings_listen::KeybindingsListen; + +pub use default_context::add_cli_context; diff --git a/nushell/crates/nu-cli/src/completions/attribute_completions.rs b/nushell/crates/nu-cli/src/completions/attribute_completions.rs new file mode 100644 index 0000000..46ba1af --- /dev/null +++ b/nushell/crates/nu-cli/src/completions/attribute_completions.rs @@ -0,0 +1,85 @@ +use super::{SemanticSuggestion, completion_options::NuMatcher}; +use crate::{ + SuggestionKind, + completions::{Completer, CompletionOptions}, +}; +use nu_protocol::{ + Span, + engine::{Stack, StateWorkingSet}, +}; +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, + span: Span, + offset: usize, + options: &CompletionOptions, + ) -> Vec { + 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, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + append_whitespace: false, + ..Default::default() + }, + 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, + span: Span, + offset: usize, + options: &CompletionOptions, + ) -> Vec { + 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()), + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + append_whitespace: false, + ..Default::default() + }, + kind: Some(SuggestionKind::Command(cmd.command_type(), None)), + }); + } + + matcher.results() + } +} diff --git a/nushell/crates/nu-cli/src/completions/base.rs b/nushell/crates/nu-cli/src/completions/base.rs new file mode 100644 index 0000000..4e70d63 --- /dev/null +++ b/nushell/crates/nu-cli/src/completions/base.rs @@ -0,0 +1,49 @@ +use crate::completions::CompletionOptions; +use nu_protocol::{ + DeclId, Span, + engine::{Stack, StateWorkingSet}, +}; +use reedline::Suggestion; + +pub trait Completer { + /// Fetch, filter, and sort completions + #[allow(clippy::too_many_arguments)] + fn fetch( + &mut self, + working_set: &StateWorkingSet, + stack: &Stack, + prefix: impl AsRef, + span: Span, + offset: usize, + options: &CompletionOptions, + ) -> Vec; +} + +#[derive(Debug, Default, PartialEq)] +pub struct SemanticSuggestion { + pub suggestion: Suggestion, + pub kind: Option, +} + +// TODO: think about name: maybe suggestion context? +#[derive(Clone, Debug, PartialEq)] +pub enum SuggestionKind { + Command(nu_protocol::engine::CommandType, Option), + Value(nu_protocol::Type), + CellPath, + Directory, + File, + Flag, + Module, + Operator, + Variable, +} + +impl From for SemanticSuggestion { + fn from(suggestion: Suggestion) -> Self { + Self { + suggestion, + ..Default::default() + } + } +} diff --git a/nushell/crates/nu-cli/src/completions/cell_path_completions.rs b/nushell/crates/nu-cli/src/completions/cell_path_completions.rs new file mode 100644 index 0000000..51ca0b5 --- /dev/null +++ b/nushell/crates/nu-cli/src/completions/cell_path_completions.rs @@ -0,0 +1,153 @@ +use std::borrow::Cow; + +use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind}; +use nu_engine::{column::get_columns, eval_variable}; +use nu_protocol::{ + ShellError, Span, Value, + ast::{Expr, Expression, FullCellPath, PathMember}, + engine::{Stack, StateWorkingSet}, + eval_const::eval_constant, +}; +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, + _span: Span, + offset: usize, + options: &CompletionOptions, + ) -> Vec { + let mut prefix_str = String::new(); + // position at dots, e.g. `$env.config.` + 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 { + // 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) + .map(Cow::into_owned) +} + +fn get_suggestions_by_value( + value: &Value, + current_span: reedline::Span, +) -> Vec { + 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![], + } +} diff --git a/nushell/crates/nu-cli/src/completions/command_completions.rs b/nushell/crates/nu-cli/src/completions/command_completions.rs new file mode 100644 index 0000000..ce74536 --- /dev/null +++ b/nushell/crates/nu-cli/src/completions/command_completions.rs @@ -0,0 +1,158 @@ +use std::collections::HashMap; + +use crate::{ + SuggestionKind, + completions::{Completer, CompletionOptions}, +}; +use nu_protocol::{ + Span, + engine::{CommandType, Stack, StateWorkingSet}, +}; +use reedline::Suggestion; + +use super::{SemanticSuggestion, completion_options::NuMatcher}; + +pub struct CommandCompletion { + /// Whether to include internal commands + pub internals: bool, + /// Whether to include external commands + pub externals: bool, +} + +impl CommandCompletion { + fn external_command_completion( + &self, + working_set: &StateWorkingSet, + sugg_span: reedline::Span, + matched_internal: impl Fn(&str) -> bool, + matcher: &mut NuMatcher, + ) -> HashMap { + let mut suggs = HashMap::new(); + + let paths = working_set.permanent_state.get_env_var_insensitive("path"); + + if let Some((_, paths)) = paths { + if let Ok(paths) = paths.as_list() { + for path in paths { + let path = path.coerce_str().unwrap_or_default(); + + if let Ok(mut contents) = std::fs::read_dir(path.as_ref()) { + while let Some(Ok(item)) = contents.next() { + if working_set + .permanent_state + .config + .completions + .external + .max_results + <= suggs.len() as i64 + { + break; + } + let Ok(name) = item.file_name().into_string() else { + continue; + }; + let value = if matched_internal(&name) { + format!("^{}", name) + } else { + name.clone() + }; + if suggs.contains_key(&value) { + continue; + } + // 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, + )), + }, + ); + } + } + } + } + } + } + + suggs + } +} + +impl Completer for CommandCompletion { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + _stack: &Stack, + prefix: impl AsRef, + span: Span, + offset: usize, + options: &CompletionOptions, + ) -> Vec { + let mut matcher = NuMatcher::new(prefix, options); + + let sugg_span = reedline::Span::new(span.start - offset, span.end - offset); + + let mut internal_suggs = HashMap::new(); + 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, + sugg_span, + |name| internal_suggs.contains_key(name), + &mut matcher, + ) + } else { + HashMap::new() + }; + + let mut res = Vec::new(); + for cmd_name in matcher.results() { + if let Some(sugg) = internal_suggs + .remove(&cmd_name) + .or_else(|| external_suggs.remove(&cmd_name)) + { + res.push(sugg); + } + } + res + } +} diff --git a/nushell/crates/nu-cli/src/completions/completer.rs b/nushell/crates/nu-cli/src/completions/completer.rs new file mode 100644 index 0000000..3de89fb --- /dev/null +++ b/nushell/crates/nu-cli/src/completions/completer.rs @@ -0,0 +1,873 @@ +use crate::completions::{ + AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer, + CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, + ExportableCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion, + base::{SemanticSuggestion, SuggestionKind}, +}; +use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style}; +use nu_engine::eval_block; +use nu_parser::{flatten_expression, parse, parse_module_file_or_dir}; +use nu_protocol::{ + PipelineData, Span, Type, Value, + ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse}, + debugger::WithoutDebug, + engine::{Closure, EngineState, Stack, StateWorkingSet}, +}; +use reedline::{Completer as ReedlineCompleter, Suggestion}; +use std::sync::Arc; + +/// Used as the function `f` in find_map Traverse +/// +/// returns the inner-most pipeline_element of interest +/// i.e. the one that contains given position and needs completion +fn find_pipeline_element_by_position<'a>( + expr: &'a Expression, + working_set: &'a StateWorkingSet, + pos: usize, +) -> FindMapResult<&'a Expression> { + // skip the entire expression if the position is not in it + if !expr.span.contains(pos) { + return FindMapResult::Stop; + } + let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos); + match &expr.expr { + Expr::Call(call) => call + .arguments + .iter() + .find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure))) + // if no inner call/external_call found, then this is the inner-most one + .or(Some(expr)) + .map(FindMapResult::Found) + .unwrap_or_default(), + Expr::ExternalCall(head, arguments) => arguments + .iter() + .find_map(|arg| arg.expr().find_map(working_set, &closure)) + .or(head.as_ref().find_map(working_set, &closure)) + .or(Some(expr)) + .map(FindMapResult::Found) + .unwrap_or_default(), + // complete the operator + Expr::BinaryOp(lhs, _, rhs) => lhs + .find_map(working_set, &closure) + .or(rhs.find_map(working_set, &closure)) + .or(Some(expr)) + .map(FindMapResult::Found) + .unwrap_or_default(), + Expr::FullCellPath(fcp) => fcp + .head + .find_map(working_set, &closure) + .map(FindMapResult::Found) + // e.g. use std/util [ + .or_else(|| { + (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_))) + .then_some(FindMapResult::Continue) + }) + .or(Some(FindMapResult::Found(expr))) + .unwrap_or_default(), + Expr::Var(_) => FindMapResult::Found(expr), + Expr::AttributeBlock(ab) => ab + .attributes + .iter() + .map(|attr| &attr.expr) + .chain(Some(ab.item.as_ref())) + .find_map(|expr| expr.find_map(working_set, &closure)) + .or(Some(expr)) + .map(FindMapResult::Found) + .unwrap_or_default(), + _ => FindMapResult::Continue, + } +} + +/// Before completion, an additional character `a` is added to the source as a placeholder for correct parsing results. +/// This function helps to strip it +fn strip_placeholder_if_any<'a>( + working_set: &'a StateWorkingSet, + span: &Span, + strip: bool, +) -> (Span, &'a [u8]) { + let new_span = if strip { + let new_end = std::cmp::max(span.end - 1, span.start); + Span::new(span.start, new_end) + } else { + span.to_owned() + }; + let prefix = working_set.get_span_contents(new_span); + (new_span, prefix) +} + +/// Given a span with noise, +/// 1. Call `rsplit` to get the last token +/// 2. Strip the last placeholder from the token +fn strip_placeholder_with_rsplit<'a>( + working_set: &'a StateWorkingSet, + span: &Span, + predicate: impl FnMut(&u8) -> bool, + strip: bool, +) -> (Span, &'a [u8]) { + let span_content = working_set.get_span_contents(*span); + let mut prefix = span_content + .rsplit(predicate) + .next() + .unwrap_or(span_content); + let start = span.end.saturating_sub(prefix.len()); + if strip && !prefix.is_empty() { + prefix = &prefix[..prefix.len() - 1]; + } + let end = start + prefix.len(); + (Span::new(start, end), prefix) +} + +#[derive(Clone)] +pub struct NuCompleter { + engine_state: Arc, + stack: Stack, +} + +/// Common arguments required for Completer +struct Context<'a> { + working_set: &'a StateWorkingSet<'a>, + span: Span, + prefix: &'a [u8], + offset: usize, +} + +/// For argument completion +struct PositionalArguments<'a> { + /// command name + command_head: &'a str, + /// indices of positional arguments + positional_arg_indices: Vec, + /// argument list + arguments: &'a [Argument], + /// expression of current argument + expr: &'a Expression, +} + +impl Context<'_> { + fn new<'a>( + working_set: &'a StateWorkingSet, + span: Span, + prefix: &'a [u8], + offset: usize, + ) -> Context<'a> { + Context { + working_set, + span, + prefix, + offset, + } + } +} + +impl NuCompleter { + pub fn new(engine_state: Arc, stack: Arc) -> Self { + Self { + engine_state, + stack: Stack::with_parent(stack).reset_out_dest().collect_value(), + } + } + + pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec { + let mut working_set = StateWorkingSet::new(&self.engine_state); + let offset = working_set.next_span_start(); + // TODO: Callers should be trimming the line themselves + let line = if line.len() > pos { &line[..pos] } else { line }; + let block = parse( + &mut working_set, + Some("completer"), + // Add a placeholder `a` to the end + format!("{}a", line).as_bytes(), + false, + ); + self.fetch_completions_by_block(block, &working_set, pos, offset, line, true) + } + + /// For completion in LSP server. + /// We don't truncate the contents in order + /// to complete the definitions after the cursor. + /// + /// And we avoid the placeholder to reuse the parsed blocks + /// cached while handling other LSP requests, e.g. diagnostics + pub fn fetch_completions_within_file( + &self, + filename: &str, + pos: usize, + contents: &str, + ) -> Vec { + let mut working_set = StateWorkingSet::new(&self.engine_state); + let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false); + let Some(file_span) = working_set.get_span_for_filename(filename) else { + return vec![]; + }; + let offset = file_span.start; + self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false) + } + + fn fetch_completions_by_block( + &self, + block: Arc, + working_set: &StateWorkingSet, + pos: usize, + offset: usize, + contents: &str, + extra_placeholder: bool, + ) -> Vec { + // Adjust offset so that the spans of the suggestions will start at the right + // place even with `only_buffer_difference: true` + let mut pos_to_search = pos + offset; + if !extra_placeholder { + pos_to_search = pos_to_search.saturating_sub(1); + } + let Some(element_expression) = block.find_map(working_set, &|expr: &Expression| { + find_pipeline_element_by_position(expr, working_set, pos_to_search) + }) else { + return vec![]; + }; + + // text of element_expression + let start_offset = element_expression.span.start - offset; + let Some(text) = contents.get(start_offset..pos) else { + return vec![]; + }; + self.complete_by_expression( + working_set, + element_expression, + offset, + pos_to_search, + text, + extra_placeholder, + ) + } + + /// Complete given the expression of interest + /// Usually, the expression is get from `find_pipeline_element_by_position` + /// + /// # Arguments + /// * `offset` - start offset of current working_set span + /// * `pos` - cursor position, should be > offset + /// * `prefix_str` - all the text before the cursor, within the `element_expression` + /// * `strip` - whether to strip the extra placeholder from a span + fn complete_by_expression( + &self, + working_set: &StateWorkingSet, + element_expression: &Expression, + offset: usize, + pos: usize, + prefix_str: &str, + strip: bool, + ) -> Vec { + let mut suggestions: Vec = vec![]; + + match &element_expression.expr { + Expr::Var(_) => { + return self.variable_names_completion_helper( + working_set, + element_expression.span, + offset, + strip, + ); + } + Expr::FullCellPath(full_cell_path) => { + // e.g. `$e` parsed as FullCellPath + // but `$e.` without placeholder should be taken as cell_path + if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') { + return self.variable_names_completion_helper( + working_set, + element_expression.span, + offset, + strip, + ); + } else { + let mut cell_path_completer = CellPathCompletion { + full_cell_path, + position: if strip { pos - 1 } else { pos }, + }; + let ctx = Context::new(working_set, Span::unknown(), &[], offset); + return self.process_completion(&mut cell_path_completer, &ctx); + } + } + Expr::BinaryOp(lhs, op, _) => { + if op.span.contains(pos) { + let mut operator_completions = OperatorCompletion { + left_hand_side: lhs.as_ref(), + }; + let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip); + let ctx = Context::new(working_set, new_span, prefix, offset); + let results = self.process_completion(&mut operator_completions, &ctx); + if !results.is_empty() { + return results; + } + } + } + Expr::AttributeBlock(ab) => { + if let Some(span) = ab.attributes.iter().find_map(|attr| { + let span = attr.expr.span; + span.contains(pos).then_some(span) + }) { + let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip); + let ctx = Context::new(working_set, new_span, prefix, offset); + return self.process_completion(&mut AttributeCompletion, &ctx); + }; + let span = ab.item.span; + if span.contains(pos) { + let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip); + let ctx = Context::new(working_set, new_span, prefix, offset); + return self.process_completion(&mut AttributableCompletion, &ctx); + } + } + + // NOTE: user defined internal commands can have any length + // e.g. `def "foo -f --ff bar"`, complete by line text + // instead of relying on the parsing result in that case + Expr::Call(_) | Expr::ExternalCall(_, _) => { + let need_externals = !prefix_str.contains(' '); + let need_internals = !prefix_str.starts_with('^'); + let mut span = element_expression.span; + if !need_internals { + span.start += 1; + }; + suggestions.extend(self.command_completion_helper( + working_set, + span, + offset, + need_internals, + need_externals, + strip, + )) + } + _ => (), + } + + // unfinished argument completion for commands + match &element_expression.expr { + Expr::Call(call) => { + // NOTE: the argument to complete is not necessarily the last one + // for lsp completion, we don't trim the text, + // so that `def`s after pos can be completed + let mut positional_arg_indices = Vec::new(); + for (arg_idx, arg) in call.arguments.iter().enumerate() { + let span = arg.span(); + if span.contains(pos) { + // if customized completion specified, it has highest priority + if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) { + // for `--foo ` and `--foo=`, the arg span should be trimmed + let (new_span, prefix) = if matches!(arg, Argument::Named(_)) { + strip_placeholder_with_rsplit( + working_set, + &span, + |b| *b == b'=' || *b == b' ', + strip, + ) + } else { + strip_placeholder_if_any(working_set, &span, strip) + }; + let ctx = Context::new(working_set, new_span, prefix, offset); + + let mut completer = CustomCompletion::new( + decl_id, + prefix_str.into(), + pos - offset, + FileCompletion, + ); + + suggestions.extend(self.process_completion(&mut completer, &ctx)); + break; + } + + // normal arguments completion + let (new_span, prefix) = + strip_placeholder_if_any(working_set, &span, strip); + let ctx = Context::new(working_set, new_span, prefix, offset); + let flag_completion_helper = || { + let mut flag_completions = FlagCompletion { + decl_id: call.decl_id, + }; + self.process_completion(&mut flag_completions, &ctx) + }; + suggestions.extend(match arg { + // flags + Argument::Named(_) | Argument::Unknown(_) + if prefix.starts_with(b"-") => + { + flag_completion_helper() + } + // only when `strip` == false + Argument::Positional(_) if prefix == b"-" => flag_completion_helper(), + // complete according to expression type and command head + Argument::Positional(expr) => { + let command_head = working_set.get_decl(call.decl_id).name(); + positional_arg_indices.push(arg_idx); + self.argument_completion_helper( + PositionalArguments { + command_head, + positional_arg_indices, + arguments: &call.arguments, + expr, + }, + pos, + &ctx, + suggestions.is_empty(), + ) + } + _ => vec![], + }); + break; + } else if !matches!(arg, Argument::Named(_)) { + positional_arg_indices.push(arg_idx); + } + } + } + Expr::ExternalCall(head, arguments) => { + for (i, arg) in arguments.iter().enumerate() { + let span = arg.expr().span; + if span.contains(pos) { + // e.g. `sudo l` + // HACK: judge by index 0 is not accurate + if i == 0 { + let external_cmd = working_set.get_span_contents(head.span); + if external_cmd == b"sudo" || external_cmd == b"doas" { + let commands = self.command_completion_helper( + working_set, + span, + offset, + true, + true, + strip, + ); + // flags of sudo/doas can still be completed by external completer + if !commands.is_empty() { + return commands; + } + } + } + // resort to external completer set in config + let config = self.engine_state.get_config(); + if let Some(closure) = config.completions.external.completer.as_ref() { + let mut text_spans: Vec = + flatten_expression(working_set, element_expression) + .iter() + .map(|(span, _)| { + let bytes = working_set.get_span_contents(*span); + String::from_utf8_lossy(bytes).to_string() + }) + .collect(); + let mut new_span = span; + // strip the placeholder + if strip { + if let Some(last) = text_spans.last_mut() { + last.pop(); + new_span = Span::new(span.start, span.end.saturating_sub(1)); + } + } + if let Some(external_result) = + self.external_completion(closure, &text_spans, offset, new_span) + { + suggestions.extend(external_result); + return suggestions; + } + } + // for external path arguments with spaces, please check issue #15790 + if suggestions.is_empty() { + let (new_span, prefix) = + strip_placeholder_if_any(working_set, &span, strip); + let ctx = Context::new(working_set, new_span, prefix, offset); + suggestions.extend(self.process_completion(&mut FileCompletion, &ctx)); + return suggestions; + } + break; + } + } + } + _ => (), + } + + // if no suggestions yet, fallback to file completion + if suggestions.is_empty() { + let (new_span, prefix) = strip_placeholder_with_rsplit( + working_set, + &element_expression.span, + |c| *c == b' ', + strip, + ); + let ctx = Context::new(working_set, new_span, prefix, offset); + suggestions.extend(self.process_completion(&mut FileCompletion, &ctx)); + } + suggestions + } + + fn variable_names_completion_helper( + &self, + working_set: &StateWorkingSet, + span: Span, + offset: usize, + strip: bool, + ) -> Vec { + let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip); + if !prefix.starts_with(b"$") { + return vec![]; + } + let ctx = Context::new(working_set, new_span, prefix, offset); + self.process_completion(&mut VariableCompletion, &ctx) + } + + fn command_completion_helper( + &self, + working_set: &StateWorkingSet, + span: Span, + offset: usize, + internals: bool, + externals: bool, + strip: bool, + ) -> Vec { + let config = self.engine_state.get_config(); + let mut command_completions = CommandCompletion { + internals, + externals: !internals || (externals && config.completions.external.enable), + }; + let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip); + let ctx = Context::new(working_set, new_span, prefix, offset); + self.process_completion(&mut command_completions, &ctx) + } + + fn argument_completion_helper( + &self, + argument_info: PositionalArguments, + pos: usize, + ctx: &Context, + need_fallback: bool, + ) -> Vec { + let PositionalArguments { + command_head, + positional_arg_indices, + arguments, + expr, + } = argument_info; + // special commands + match command_head { + // complete module file/directory + "use" | "export use" | "overlay use" | "source-env" + if positional_arg_indices.len() == 1 => + { + return self.process_completion( + &mut DotNuCompletion { + std_virtual_path: command_head != "source-env", + }, + ctx, + ); + } + // NOTE: if module file already specified, + // should parse it to get modules/commands/consts to complete + "use" | "export use" => { + let Some(Argument::Positional(Expression { + expr: Expr::String(module_name), + span, + .. + })) = positional_arg_indices + .first() + .and_then(|i| arguments.get(*i)) + else { + return vec![]; + }; + let module_name = module_name.as_bytes(); + let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) { + Some(module_id) => (module_id, None), + None => { + let mut temp_working_set = + StateWorkingSet::new(ctx.working_set.permanent_state); + let Some(module_id) = parse_module_file_or_dir( + &mut temp_working_set, + module_name, + *span, + None, + ) else { + return vec![]; + }; + (module_id, Some(temp_working_set)) + } + }; + let mut exportable_completion = ExportableCompletion { + module_id, + temp_working_set, + }; + let mut complete_on_list_items = |items: &[ListItem]| -> Vec { + for item in items { + let span = item.expr().span; + if span.contains(pos) { + let offset = span.start.saturating_sub(ctx.span.start); + let end_offset = + ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1); + let new_ctx = Context::new( + ctx.working_set, + Span::new(span.start, ctx.span.end.min(span.end)), + ctx.prefix.get(offset..end_offset).unwrap_or_default(), + ctx.offset, + ); + return self.process_completion(&mut exportable_completion, &new_ctx); + } + } + vec![] + }; + + match &expr.expr { + Expr::String(_) => { + return self.process_completion(&mut exportable_completion, ctx); + } + Expr::FullCellPath(fcp) => match &fcp.head.expr { + Expr::List(items) => { + return complete_on_list_items(items); + } + _ => return vec![], + }, + _ => return vec![], + } + } + "which" => { + let mut completer = CommandCompletion { + internals: true, + externals: true, + }; + return self.process_completion(&mut completer, ctx); + } + _ => (), + } + + // general positional arguments + let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx); + match &expr.expr { + Expr::Directory(_, _) => self.process_completion(&mut DirectoryCompletion, ctx), + Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(), + // fallback to file completion if necessary + _ if need_fallback => file_completion_helper(), + _ => vec![], + } + } + + // Process the completion for a given completer + fn process_completion( + &self, + completer: &mut T, + ctx: &Context, + ) -> Vec { + let config = self.engine_state.get_config(); + + let options = CompletionOptions { + case_sensitive: config.completions.case_sensitive, + match_algorithm: config.completions.algorithm.into(), + sort: config.completions.sort, + }; + + completer.fetch( + ctx.working_set, + &self.stack, + String::from_utf8_lossy(ctx.prefix), + ctx.span, + ctx.offset, + &options, + ) + } + + fn external_completion( + &self, + closure: &Closure, + spans: &[String], + offset: usize, + span: Span, + ) -> Option> { + let block = self.engine_state.get_block(closure.block_id); + let mut callee_stack = self + .stack + .captures_to_stack_preserve_out_dest(closure.captures.clone()); + + // Line + if let Some(pos_arg) = block.signature.required_positional.first() { + if let Some(var_id) = pos_arg.var_id { + callee_stack.add_var( + var_id, + Value::list( + spans + .iter() + .map(|it| Value::string(it, Span::unknown())) + .collect(), + Span::unknown(), + ), + ); + } + } + + let result = eval_block::( + &self.engine_state, + &mut callee_stack, + block, + PipelineData::empty(), + ); + + match result.and_then(|data| data.into_value(span)) { + Ok(Value::List { vals, .. }) => { + let result = + map_value_completions(vals.iter(), Span::new(span.start, span.end), offset); + Some(result) + } + Ok(Value::Nothing { .. }) => None, + Ok(value) => { + log::error!( + "External completer returned invalid value of type {}", + value.get_type().to_string() + ); + Some(vec![]) + } + Err(err) => { + log::error!("failed to eval completer block: {err}"); + Some(vec![]) + } + } + } +} + +impl ReedlineCompleter for NuCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec { + self.fetch_completions_at(line, pos) + .into_iter() + .map(|s| s.suggestion) + .collect() + } +} + +pub fn map_value_completions<'a>( + list: impl Iterator, + span: Span, + offset: usize, +) -> Vec { + list.filter_map(move |x| { + // Match for string values + if let Ok(s) = x.coerce_string() { + return Some(SemanticSuggestion { + suggestion: Suggestion { + value: s, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + ..Suggestion::default() + }, + kind: Some(SuggestionKind::Value(x.get_type())), + }); + } + + // Match for record values + if let Ok(record) = x.as_record() { + let mut suggestion = Suggestion { + value: String::from(""), // Initialize with empty string + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + ..Suggestion::default() + }; + let mut value_type = Type::String; + + // Iterate the cols looking for `value` and `description` + record.iter().for_each(|(key, value)| { + match key.as_str() { + "value" => { + value_type = value.get_type(); + // Convert the value to string + if let Ok(val_str) = value.coerce_string() { + // Update the suggestion value + suggestion.value = val_str; + } + } + "description" => { + // Convert the value to string + if let Ok(desc_str) = value.coerce_string() { + // Update the suggestion value + suggestion.description = Some(desc_str); + } + } + "style" => { + // Convert the value to string + suggestion.style = match value { + Value::String { val, .. } => Some(lookup_ansi_color_style(val)), + Value::Record { .. } => Some(color_record_to_nustyle(value)), + _ => None, + }; + } + _ => (), + } + }); + + return Some(SemanticSuggestion { + suggestion, + kind: Some(SuggestionKind::Value(value_type)), + }); + } + + None + }) + .collect() +} + +#[cfg(test)] +mod completer_tests { + use super::*; + + #[test] + fn test_completion_helper() { + let mut engine_state = + nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()); + + // Custom additions + let delta = { + let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state); + working_set.render() + }; + + let result = engine_state.merge_delta(delta); + assert!( + result.is_ok(), + "Error merging delta: {:?}", + result.err().unwrap() + ); + + let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new())); + let dataset = [ + ("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]), + ("1.0 bit-sh", false, "b", vec![]), + ("1 m", true, "m", vec!["mod"]), + ("1.0 m", true, "m", vec!["mod"]), + ("\"a\" s", true, "s", vec!["starts-with"]), + ("sudo", false, "", Vec::new()), + ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]), + (" sudo", false, "", Vec::new()), + (" sudo le", true, "le", vec!["let", "length"]), + ( + "ls | c", + true, + "c", + vec!["cd", "config", "const", "cp", "cal"], + ), + ("ls | sudo m", true, "m", vec!["mv", "mut", "move"]), + ]; + for (line, has_result, begins_with, expected_values) in dataset { + let result = completer.fetch_completions_at(line, line.len()); + // Test whether the result is empty or not + assert_eq!(!result.is_empty(), has_result, "line: {}", line); + + // Test whether the result begins with the expected value + result + .iter() + .for_each(|x| assert!(x.suggestion.value.starts_with(begins_with))); + + // Test whether the result contains all the expected values + assert_eq!( + result + .iter() + .map(|x| expected_values.contains(&x.suggestion.value.as_str())) + .filter(|x| *x) + .count(), + expected_values.len(), + "line: {}", + line + ); + } + } +} diff --git a/nushell/crates/nu-cli/src/completions/completion_common.rs b/nushell/crates/nu-cli/src/completions/completion_common.rs new file mode 100644 index 0000000..41f6c0c --- /dev/null +++ b/nushell/crates/nu-cli/src/completions/completion_common.rs @@ -0,0 +1,401 @@ +use super::{MatchAlgorithm, completion_options::NuMatcher}; +use crate::completions::CompletionOptions; +use nu_ansi_term::Style; +use nu_engine::env_to_string; +use nu_path::dots::expand_ndots; +use nu_path::{expand_to_real_path, home_dir}; +use nu_protocol::{ + Span, + engine::{EngineState, Stack, StateWorkingSet}, +}; +use nu_utils::IgnoreCaseExt; +use nu_utils::get_ls_colors; +use std::path::{Component, MAIN_SEPARATOR as SEP, Path, PathBuf, is_separator}; + +#[derive(Clone, Default)] +pub struct PathBuiltFromString { + cwd: PathBuf, + parts: Vec, + 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( + partial: &[&str], + built_paths: &[PathBuiltFromString], + options: &CompletionOptions, + want_directory: bool, + isdir: bool, + enable_exact_match: bool, +) -> Vec { + let has_more = !partial.is_empty() && (partial.len() > 1 || isdir); + + if let Some((&base, rest)) = partial.split_first() { + if base.chars().all(|c| c == '.') && has_more { + 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); + + let mut exact_match = None; + // Only relevant for case insensitive matching + let mut multiple_exact_matches = false; + for built in built_paths { + let mut path = built.cwd.clone(); + for part in &built.parts { + path.push(part); + } + + let Ok(result) = path.read_dir() else { + continue; + }; + + for entry in result.filter_map(|e| e.ok()) { + let entry_name = entry.file_name().to_string_lossy().into_owned(); + let entry_isdir = entry.path().is_dir(); + 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 enable_exact_match && !multiple_exact_matches && has_more { + let matches = if options.case_sensitive { + entry_name.eq(prefix) + } else { + entry_name.eq_ignore_case(prefix) + }; + if matches { + if exact_match.is_none() { + exact_match = Some(built.clone()); + } else { + multiple_exact_matches = true; + } + } + } + + matcher.add(entry_name, built); + } + } + } + + // Don't show longer completions if we have a single exact match (#13204, #14794) + if !multiple_exact_matches { + if let Some(built) = exact_match { + return complete_rec( + &partial[1..], + &[built], + options, + want_directory, + isdir, + true, + ); + } + } + + if has_more { + let mut completions = vec![]; + for built in matcher.results() { + completions.extend(complete_rec( + &partial[1..], + &[built], + options, + want_directory, + isdir, + false, + )); + } + completions + } else { + matcher.results() + } +} + +#[derive(Debug)] +enum OriginalCwd { + None, + Home, + Prefix(String), +} + +impl OriginalCwd { + fn apply(&self, mut p: PathBuiltFromString, path_separator: char) -> String { + match self { + Self::None => {} + Self::Home => p.parts.insert(0, "~".to_string()), + Self::Prefix(s) => p.parts.insert(0, s.clone()), + }; + + let mut ret = p.parts.join(&path_separator.to_string()); + if p.isdir { + ret.push(path_separator); + } + ret + } +} + +pub fn surround_remove(partial: &str) -> String { + for c in ['`', '"', '\''] { + if partial.starts_with(c) { + let ret = partial.strip_prefix(c).unwrap_or(partial); + return match ret.split(c).collect::>()[..] { + [inside] => inside.to_string(), + [inside, outside] if inside.ends_with(is_separator) => format!("{inside}{outside}"), + _ => ret.to_string(), + }; + } + } + partial.to_string() +} + +pub struct FileSuggestion { + pub span: nu_protocol::Span, + pub path: String, + pub style: Option
foobar
12
"#, + )), + }, + Example { + description: "Optionally, only output the html for the content itself", + example: "[[foo bar]; [1 2]] | to html --partial", + result: Some(Value::test_string( + r#"
foobar
12
"#, + )), + }, + Example { + description: "Optionally, output the string with a dark background", + example: "[[foo bar]; [1 2]] | to html --dark", + result: Some(Value::test_string( + r#"
foobar
12
"#, + )), + }, + ] + } + + fn description(&self) -> &str { + "Convert table into simple HTML." + } + + fn extra_description(&self) -> &str { + "Screenshots of the themes can be browsed here: https://github.com/mbadolato/iTerm2-Color-Schemes." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + to_html(input, call, engine_state, stack) + } +} + +fn get_theme_from_asset_file( + is_dark: bool, + theme: Option<&Spanned>, +) -> Result, ShellError> { + let theme_name = match theme { + Some(s) => &s.item, + None => { + return Ok(convert_html_theme_to_hash_map( + is_dark, + &HtmlTheme::default(), + )); + } + }; + + let theme_span = theme.map(|s| s.span).unwrap_or(Span::unknown()); + + // 228 themes come from + // https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/windowsterminal + // we should find a hit on any name in there + let asset = get_html_themes("228_themes.json").unwrap_or_default(); + + // Find the theme by theme name + let th = asset + .themes + .into_iter() + .find(|n| n.name.eq_ignore_case(theme_name)); // case insensitive search + + let th = match th { + Some(t) => t, + None => { + return Err(ShellError::TypeMismatch { + err_message: format!("Unknown HTML theme '{}'", theme_name), + span: theme_span, + }); + } + }; + + Ok(convert_html_theme_to_hash_map(is_dark, &th)) +} + +fn convert_html_theme_to_hash_map( + is_dark: bool, + theme: &HtmlTheme, +) -> HashMap<&'static str, String> { + let mut hm: HashMap<&str, String> = HashMap::with_capacity(18); + + hm.insert("bold_black", theme.brightBlack[..].to_string()); + hm.insert("bold_red", theme.brightRed[..].to_string()); + hm.insert("bold_green", theme.brightGreen[..].to_string()); + hm.insert("bold_yellow", theme.brightYellow[..].to_string()); + hm.insert("bold_blue", theme.brightBlue[..].to_string()); + hm.insert("bold_magenta", theme.brightPurple[..].to_string()); + hm.insert("bold_cyan", theme.brightCyan[..].to_string()); + hm.insert("bold_white", theme.brightWhite[..].to_string()); + + hm.insert("black", theme.black[..].to_string()); + hm.insert("red", theme.red[..].to_string()); + hm.insert("green", theme.green[..].to_string()); + hm.insert("yellow", theme.yellow[..].to_string()); + hm.insert("blue", theme.blue[..].to_string()); + hm.insert("magenta", theme.purple[..].to_string()); + hm.insert("cyan", theme.cyan[..].to_string()); + hm.insert("white", theme.white[..].to_string()); + + // Try to make theme work with light or dark but + // flipping the foreground and background but leave + // the other colors the same. + if is_dark { + hm.insert("background", theme.black[..].to_string()); + hm.insert("foreground", theme.white[..].to_string()); + } else { + hm.insert("background", theme.white[..].to_string()); + hm.insert("foreground", theme.black[..].to_string()); + } + + hm +} + +fn get_html_themes(json_name: &str) -> Result> { + match Assets::get(json_name) { + Some(content) => Ok(nu_json::from_slice(&content.data)?), + None => Ok(HtmlThemes::default()), + } +} + +fn to_html( + input: PipelineData, + call: &Call, + engine_state: &EngineState, + stack: &mut Stack, +) -> Result { + let head = call.head; + let html_color = call.has_flag(engine_state, stack, "html-color")?; + let no_color = call.has_flag(engine_state, stack, "no-color")?; + let dark = call.has_flag(engine_state, stack, "dark")?; + let partial = call.has_flag(engine_state, stack, "partial")?; + let list = call.has_flag(engine_state, stack, "list")?; + let theme: Option> = call.get_flag(engine_state, stack, "theme")?; + let config = &stack.get_config(engine_state); + + let vec_of_values = input.into_iter().collect::>(); + let headers = merge_descriptors(&vec_of_values); + let headers = Some(headers) + .filter(|headers| !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty())); + let mut output_string = String::new(); + let mut regex_hm: HashMap = HashMap::with_capacity(17); + + if list { + // Being essentially a 'help' option, this can afford to be relatively unoptimised + return Ok(theme_demo(head)); + } + let theme_span = match &theme { + Some(v) => v.span, + None => head, + }; + + let color_hm = match get_theme_from_asset_file(dark, theme.as_ref()) { + Ok(c) => c, + Err(e) => match e { + ShellError::TypeMismatch { + err_message, + span: _, + } => { + return Err(ShellError::TypeMismatch { + err_message, + span: theme_span, + }); + } + _ => return Err(e), + }, + }; + + // change the color of the page + if !partial { + write!( + &mut output_string, + r"", + color_hm + .get("background") + .expect("Error getting background color"), + color_hm + .get("foreground") + .expect("Error getting foreground color") + ) + .unwrap(); + } else { + write!( + &mut output_string, + "
", + color_hm + .get("background") + .expect("Error getting background color"), + color_hm + .get("foreground") + .expect("Error getting foreground color") + ) + .unwrap(); + } + + let inner_value = match vec_of_values.len() { + 0 => String::default(), + 1 => match headers { + Some(headers) => html_table(vec_of_values, headers, config), + None => { + let value = &vec_of_values[0]; + html_value(value.clone(), config) + } + }, + _ => match headers { + Some(headers) => html_table(vec_of_values, headers, config), + None => html_list(vec_of_values, config), + }, + }; + + output_string.push_str(&inner_value); + + if !partial { + output_string.push_str(""); + } else { + output_string.push_str("
") + } + + // Check to see if we want to remove all color or change ansi to html colors + if html_color { + setup_html_color_regexes(&mut regex_hm, &color_hm); + output_string = run_regexes(®ex_hm, &output_string); + } else if no_color { + setup_no_color_regexes(&mut regex_hm); + output_string = run_regexes(®ex_hm, &output_string); + } + + let metadata = PipelineMetadata { + data_source: nu_protocol::DataSource::None, + content_type: Some(mime::TEXT_HTML_UTF_8.to_string()), + }; + + Ok(Value::string(output_string, head).into_pipeline_data_with_metadata(metadata)) +} + +fn theme_demo(span: Span) -> PipelineData { + // If asset doesn't work, make sure to return the default theme + let html_themes = get_html_themes("228_themes.json").unwrap_or_default(); + let result: Vec = html_themes + .themes + .into_iter() + .map(|n| { + Value::record( + record! { + "name" => Value::string(n.name, span), + "black" => Value::string(n.black, span), + "red" => Value::string(n.red, span), + "green" => Value::string(n.green, span), + "yellow" => Value::string(n.yellow, span), + "blue" => Value::string(n.blue, span), + "purple" => Value::string(n.purple, span), + "cyan" => Value::string(n.cyan, span), + "white" => Value::string(n.white, span), + "brightBlack" => Value::string(n.brightBlack, span), + "brightRed" => Value::string(n.brightRed, span), + "brightGreen" => Value::string(n.brightGreen, span), + "brightYellow" => Value::string(n.brightYellow, span), + "brightBlue" => Value::string(n.brightBlue, span), + "brightPurple" => Value::string(n.brightPurple, span), + "brightCyan" => Value::string(n.brightCyan, span), + "brightWhite" => Value::string(n.brightWhite, span), + "background" => Value::string(n.background, span), + "foreground" => Value::string(n.foreground, span), + }, + span, + ) + }) + .collect(); + Value::list(result, span).into_pipeline_data_with_metadata(PipelineMetadata { + data_source: DataSource::HtmlThemes, + content_type: None, + }) +} + +fn html_list(list: Vec, config: &Config) -> String { + let mut output_string = String::new(); + output_string.push_str("
    "); + for value in list { + output_string.push_str("
  1. "); + output_string.push_str(&html_value(value, config)); + output_string.push_str("
  2. "); + } + output_string.push_str("
"); + output_string +} + +fn html_table(table: Vec, headers: Vec, config: &Config) -> String { + let mut output_string = String::new(); + + output_string.push_str(""); + + output_string.push_str(""); + for header in &headers { + output_string.push_str(""); + } + output_string.push_str(""); + + for row in table { + let span = row.span(); + if let Value::Record { val: row, .. } = row { + output_string.push_str(""); + for header in &headers { + let data = row + .get(header) + .cloned() + .unwrap_or_else(|| Value::nothing(span)); + output_string.push_str(""); + } + output_string.push_str(""); + } + } + output_string.push_str("
"); + output_string.push_str(&v_htmlescape::escape(header).to_string()); + output_string.push_str("
"); + output_string.push_str(&html_value(data, config)); + output_string.push_str("
"); + + output_string +} + +fn html_value(value: Value, config: &Config) -> String { + let mut output_string = String::new(); + match value { + Value::Binary { val, .. } => { + let output = nu_pretty_hex::pretty_hex(&val); + output_string.push_str("
");
+            output_string.push_str(&output);
+            output_string.push_str("
"); + } + other => output_string.push_str( + &v_htmlescape::escape(&other.to_abbreviated_string(config)) + .to_string() + .replace('\n', "
"), + ), + } + output_string +} + +fn setup_html_color_regexes( + hash: &mut HashMap, + color_hm: &HashMap<&str, String>, +) { + // All the bold colors + hash.insert( + 0, + ( + r"(?P\[0m)(?P[[:alnum:][:space:][:punct:]]*)", + // Reset the text color, normal weight font + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting reset text color") + ), + ), + ); + hash.insert( + 1, + ( + // Bold Black + r"(?P\[1;30m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting bold black text color") + ), + ), + ); + hash.insert( + 2, + ( + // Bold Red + r"(?P
\[1;31m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_red") + .expect("Error getting bold red text color"), + ), + ), + ); + hash.insert( + 3, + ( + // Bold Green + r"(?P\[1;32m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_green") + .expect("Error getting bold green text color"), + ), + ), + ); + hash.insert( + 4, + ( + // Bold Yellow + r"(?P\[1;33m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_yellow") + .expect("Error getting bold yellow text color"), + ), + ), + ); + hash.insert( + 5, + ( + // Bold Blue + r"(?P\[1;34m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_blue") + .expect("Error getting bold blue text color"), + ), + ), + ); + hash.insert( + 6, + ( + // Bold Magenta + r"(?P\[1;35m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_magenta") + .expect("Error getting bold magenta text color"), + ), + ), + ); + hash.insert( + 7, + ( + // Bold Cyan + r"(?P\[1;36m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_cyan") + .expect("Error getting bold cyan text color"), + ), + ), + ); + hash.insert( + 8, + ( + // Bold White + // Let's change this to black since the html background + // is white. White on white = no bueno. + r"(?P\[1;37m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting bold bold white text color"), + ), + ), + ); + // All the normal colors + hash.insert( + 9, + ( + // Black + r"(?P\[30m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting black text color"), + ), + ), + ); + hash.insert( + 10, + ( + // Red + r"(?P\[31m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm.get("red").expect("Error getting red text color"), + ), + ), + ); + hash.insert( + 11, + ( + // Green + r"(?P\[32m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("green") + .expect("Error getting green text color"), + ), + ), + ); + hash.insert( + 12, + ( + // Yellow + r"(?P\[33m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("yellow") + .expect("Error getting yellow text color"), + ), + ), + ); + hash.insert( + 13, + ( + // Blue + r"(?P\[34m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm.get("blue").expect("Error getting blue text color"), + ), + ), + ); + hash.insert( + 14, + ( + // Magenta + r"(?P\[35m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("magenta") + .expect("Error getting magenta text color"), + ), + ), + ); + hash.insert( + 15, + ( + // Cyan + r"(?P\[36m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm.get("cyan").expect("Error getting cyan text color"), + ), + ), + ); + hash.insert( + 16, + ( + // White + // Let's change this to black since the html background + // is white. White on white = no bueno. + r"(?P\[37m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting white text color"), + ), + ), + ); +} + +fn setup_no_color_regexes(hash: &mut HashMap) { + // We can just use one regex here because we're just removing ansi sequences + // and not replacing them with html colors. + // attribution: https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python + hash.insert( + 0, + ( + r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])", + r"$name_group_doesnt_exist".to_string(), + ), + ); +} + +fn run_regexes(hash: &HashMap, contents: &str) -> String { + let mut working_string = contents.to_owned(); + let hash_count: u32 = hash.len() as u32; + for n in 0..hash_count { + let value = hash.get(&n).expect("error getting hash at index"); + let re = Regex::new(value.0).expect("problem with color regex"); + let after = re.replace_all(&working_string, &value.1[..]).to_string(); + working_string = after; + } + working_string +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToHtml {}) + } + + #[test] + fn get_theme_from_asset_file_returns_default() { + let result = super::get_theme_from_asset_file(false, None); + + assert!(result.is_ok(), "Expected Ok result for None theme"); + + let theme_map = result.unwrap(); + + assert_eq!( + theme_map.get("background").map(String::as_str), + Some("white"), + "Expected default background color to be white" + ); + + assert_eq!( + theme_map.get("foreground").map(String::as_str), + Some("black"), + "Expected default foreground color to be black" + ); + + assert!( + theme_map.contains_key("red"), + "Expected default theme to have a 'red' color" + ); + + assert!( + theme_map.contains_key("bold_green"), + "Expected default theme to have a 'bold_green' color" + ); + } + + #[test] + fn returns_a_valid_theme() { + let theme_name = "Dracula".to_string().into_spanned(Span::new(0, 7)); + let result = super::get_theme_from_asset_file(false, Some(&theme_name)); + + assert!(result.is_ok(), "Expected Ok result for valid theme"); + let theme_map = result.unwrap(); + let required_keys = [ + "background", + "foreground", + "red", + "green", + "blue", + "bold_red", + "bold_green", + "bold_blue", + ]; + + for key in required_keys { + assert!( + theme_map.contains_key(key), + "Expected theme to contain key '{}'", + key + ); + } + } + + #[test] + fn fails_with_unknown_theme_name() { + let result = super::get_theme_from_asset_file( + false, + Some(&"doesnt-exist".to_string().into_spanned(Span::new(0, 13))), + ); + + assert!(result.is_err(), "Expected error for invalid theme name"); + + if let Err(err) = result { + assert!( + matches!(err, ShellError::TypeMismatch { .. }), + "Expected TypeMismatch error, got: {:?}", + err + ); + + if let ShellError::TypeMismatch { err_message, span } = err { + assert!( + err_message.contains("doesnt-exist"), + "Error message should mention theme name, got: {}", + err_message + ); + assert_eq!(span.start, 0); + assert_eq!(span.end, 13); + } + } + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/formats/to/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/formats/to/mod.rs new file mode 100644 index 0000000..558c29b --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/formats/to/mod.rs @@ -0,0 +1 @@ +pub(crate) mod html; diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/arccos.rs b/nushell/crates/nu-cmd-extra/src/extra/math/arccos.rs new file mode 100644 index 0000000..bf7dd18 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/arccos.rs @@ -0,0 +1,119 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathArcCos; + +impl Command for MathArcCos { + fn name(&self) -> &str { + "math arccos" + } + + fn signature(&self) -> Signature { + Signature::build("math arccos") + .switch("degrees", "Return degrees instead of radians", Some('d')) + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the arccosine of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "inverse"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let use_degrees = call.has_flag(engine_state, stack, "degrees")?; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| operate(value, head, use_degrees), + engine_state.signals(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get the arccosine of 1", + example: "1 | math arccos", + result: Some(Value::test_float(0.0f64)), + }, + Example { + description: "Get the arccosine of -1 in degrees", + example: "-1 | math arccos --degrees", + result: Some(Value::test_float(180.0)), + }, + ] + } +} + +fn operate(value: Value, head: Span, use_degrees: bool) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + if (-1.0..=1.0).contains(&val) { + let val = val.acos(); + let val = if use_degrees { val.to_degrees() } else { val }; + + Value::float(val, span) + } else { + Value::error( + ShellError::UnsupportedInput { + msg: "'arccos' undefined for values outside the closed interval [-1, 1]." + .into(), + input: "value originates from here".into(), + msg_span: head, + input_span: span, + }, + span, + ) + } + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathArcCos {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/arccosh.rs b/nushell/crates/nu-cmd-extra/src/extra/math/arccosh.rs new file mode 100644 index 0000000..5f8a5f6 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/arccosh.rs @@ -0,0 +1,105 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathArcCosH; + +impl Command for MathArcCosH { + fn name(&self) -> &str { + "math arccosh" + } + + fn signature(&self) -> Signature { + Signature::build("math arccosh") + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the inverse of the hyperbolic cosine function." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "inverse", "hyperbolic"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| operate(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the arccosh of 1", + example: "1 | math arccosh", + result: Some(Value::test_float(0.0f64)), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + if (1.0..).contains(&val) { + let val = val.acosh(); + + Value::float(val, span) + } else { + Value::error( + ShellError::UnsupportedInput { + msg: "'arccosh' undefined for values below 1.".into(), + input: "value originates from here".into(), + msg_span: head, + input_span: span, + }, + span, + ) + } + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathArcCosH {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/arcsin.rs b/nushell/crates/nu-cmd-extra/src/extra/math/arcsin.rs new file mode 100644 index 0000000..817438d --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/arcsin.rs @@ -0,0 +1,120 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathArcSin; + +impl Command for MathArcSin { + fn name(&self) -> &str { + "math arcsin" + } + + fn signature(&self) -> Signature { + Signature::build("math arcsin") + .switch("degrees", "Return degrees instead of radians", Some('d')) + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the arcsine of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "inverse"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let use_degrees = call.has_flag(engine_state, stack, "degrees")?; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| operate(value, head, use_degrees), + engine_state.signals(), + ) + } + + fn examples(&self) -> Vec { + let pi = std::f64::consts::PI; + vec![ + Example { + description: "Get the arcsine of 1", + example: "1 | math arcsin", + result: Some(Value::test_float(pi / 2.0)), + }, + Example { + description: "Get the arcsine of 1 in degrees", + example: "1 | math arcsin --degrees", + result: Some(Value::test_float(90.0)), + }, + ] + } +} + +fn operate(value: Value, head: Span, use_degrees: bool) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + if (-1.0..=1.0).contains(&val) { + let val = val.asin(); + let val = if use_degrees { val.to_degrees() } else { val }; + + Value::float(val, span) + } else { + Value::error( + ShellError::UnsupportedInput { + msg: "'arcsin' undefined for values outside the closed interval [-1, 1]." + .into(), + input: "value originates from here".into(), + msg_span: head, + input_span: span, + }, + span, + ) + } + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathArcSin {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/arcsinh.rs b/nushell/crates/nu-cmd-extra/src/extra/math/arcsinh.rs new file mode 100644 index 0000000..6faa2dd --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/arcsinh.rs @@ -0,0 +1,93 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathArcSinH; + +impl Command for MathArcSinH { + fn name(&self) -> &str { + "math arcsinh" + } + + fn signature(&self) -> Signature { + Signature::build("math arcsinh") + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the inverse of the hyperbolic sine function." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "inverse", "hyperbolic"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| operate(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the arcsinh of 0", + example: "0 | math arcsinh", + result: Some(Value::test_float(0.0f64)), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + let val = val.asinh(); + + Value::float(val, span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathArcSinH {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/arctan.rs b/nushell/crates/nu-cmd-extra/src/extra/math/arctan.rs new file mode 100644 index 0000000..17de98b --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/arctan.rs @@ -0,0 +1,107 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathArcTan; + +impl Command for MathArcTan { + fn name(&self) -> &str { + "math arctan" + } + + fn signature(&self) -> Signature { + Signature::build("math arctan") + .switch("degrees", "Return degrees instead of radians", Some('d')) + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the arctangent of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "inverse"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let use_degrees = call.has_flag(engine_state, stack, "degrees")?; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| operate(value, head, use_degrees), + engine_state.signals(), + ) + } + + fn examples(&self) -> Vec { + let pi = std::f64::consts::PI; + vec![ + Example { + description: "Get the arctangent of 1", + example: "1 | math arctan", + result: Some(Value::test_float(pi / 4.0f64)), + }, + Example { + description: "Get the arctangent of -1 in degrees", + example: "-1 | math arctan --degrees", + result: Some(Value::test_float(-45.0)), + }, + ] + } +} + +fn operate(value: Value, head: Span, use_degrees: bool) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + let val = val.atan(); + let val = if use_degrees { val.to_degrees() } else { val }; + + Value::float(val, span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathArcTan {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/arctanh.rs b/nushell/crates/nu-cmd-extra/src/extra/math/arctanh.rs new file mode 100644 index 0000000..97fb727 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/arctanh.rs @@ -0,0 +1,106 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathArcTanH; + +impl Command for MathArcTanH { + fn name(&self) -> &str { + "math arctanh" + } + + fn signature(&self) -> Signature { + Signature::build("math arctanh") + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the inverse of the hyperbolic tangent function." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "inverse", "hyperbolic"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| operate(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the arctanh of 1", + example: "1 | math arctanh", + result: Some(Value::test_float(f64::INFINITY)), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + if (-1.0..=1.0).contains(&val) { + let val = val.atanh(); + + Value::float(val, span) + } else { + Value::error( + ShellError::UnsupportedInput { + msg: "'arctanh' undefined for values outside the open interval (-1, 1)." + .into(), + input: "value originates from here".into(), + msg_span: head, + input_span: span, + }, + head, + ) + } + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathArcTanH {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/cos.rs b/nushell/crates/nu-cmd-extra/src/extra/math/cos.rs new file mode 100644 index 0000000..c5d7763 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/cos.rs @@ -0,0 +1,113 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathCos; + +impl Command for MathCos { + fn name(&self) -> &str { + "math cos" + } + + fn signature(&self) -> Signature { + Signature::build("math cos") + .switch("degrees", "Use degrees instead of radians", Some('d')) + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the cosine of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let use_degrees = call.has_flag(engine_state, stack, "degrees")?; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| operate(value, head, use_degrees), + engine_state.signals(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Apply the cosine to Ï€", + example: "3.141592 | math cos | math round --precision 4", + result: Some(Value::test_float(-1f64)), + }, + Example { + description: "Apply the cosine to a list of angles in degrees", + example: "[0 90 180 270 360] | math cos --degrees", + result: Some(Value::list( + vec![ + Value::test_float(1f64), + Value::test_float(0f64), + Value::test_float(-1f64), + Value::test_float(0f64), + Value::test_float(1f64), + ], + Span::test_data(), + )), + }, + ] + } +} + +fn operate(value: Value, head: Span, use_degrees: bool) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + let val = if use_degrees { val.to_radians() } else { val }; + + Value::float(val.cos(), span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathCos {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/cosh.rs b/nushell/crates/nu-cmd-extra/src/extra/math/cosh.rs new file mode 100644 index 0000000..46ec992 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/cosh.rs @@ -0,0 +1,93 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathCosH; + +impl Command for MathCosH { + fn name(&self) -> &str { + "math cosh" + } + + fn signature(&self) -> Signature { + Signature::build("math cosh") + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the hyperbolic cosine of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "hyperbolic"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| operate(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + let e = std::f64::consts::E; + vec![Example { + description: "Apply the hyperbolic cosine to 1", + example: "1 | math cosh", + result: Some(Value::test_float(((e * e) + 1.0) / (2.0 * e))), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + Value::float(val.cosh(), span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathCosH {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/exp.rs b/nushell/crates/nu-cmd-extra/src/extra/math/exp.rs new file mode 100644 index 0000000..b8954bd --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/exp.rs @@ -0,0 +1,98 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathExp; + +impl Command for MathExp { + fn name(&self) -> &str { + "math exp" + } + + fn signature(&self) -> Signature { + Signature::build("math exp") + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns e raised to the power of x." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["exponential", "exponentiation", "euler"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| operate(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get e raised to the power of zero", + example: "0 | math exp", + result: Some(Value::test_float(1.0f64)), + }, + Example { + description: "Get e (same as 'math e')", + example: "1 | math exp", + result: Some(Value::test_float(1.0f64.exp())), + }, + ] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + Value::float(val.exp(), span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathExp {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/ln.rs b/nushell/crates/nu-cmd-extra/src/extra/math/ln.rs new file mode 100644 index 0000000..60890fa --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/ln.rs @@ -0,0 +1,105 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathLn; + +impl Command for MathLn { + fn name(&self) -> &str { + "math ln" + } + + fn signature(&self) -> Signature { + Signature::build("math ln") + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the natural logarithm. Base: (math e)." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["natural", "logarithm", "inverse", "euler"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| operate(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the natural logarithm of e", + example: "2.7182818 | math ln | math round --precision 4", + result: Some(Value::test_float(1.0f64)), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + if val > 0.0 { + let val = val.ln(); + + Value::float(val, span) + } else { + Value::error( + ShellError::UnsupportedInput { + msg: "'ln' undefined for values outside the open interval (0, Inf).".into(), + input: "value originates from here".into(), + msg_span: head, + input_span: span, + }, + span, + ) + } + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathLn {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/math/mod.rs new file mode 100644 index 0000000..239a57d --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/mod.rs @@ -0,0 +1,33 @@ +mod cos; +mod cosh; +mod sin; +mod sinh; +mod tan; +mod tanh; + +mod exp; +mod ln; + +mod arccos; +mod arccosh; +mod arcsin; +mod arcsinh; +mod arctan; +mod arctanh; + +pub use cos::MathCos; +pub use cosh::MathCosH; +pub use sin::MathSin; +pub use sinh::MathSinH; +pub use tan::MathTan; +pub use tanh::MathTanH; + +pub use exp::MathExp; +pub use ln::MathLn; + +pub use arccos::MathArcCos; +pub use arccosh::MathArcCosH; +pub use arcsin::MathArcSin; +pub use arcsinh::MathArcSinH; +pub use arctan::MathArcTan; +pub use arctanh::MathArcTanH; diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/sin.rs b/nushell/crates/nu-cmd-extra/src/extra/math/sin.rs new file mode 100644 index 0000000..e22dea8 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/sin.rs @@ -0,0 +1,113 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathSin; + +impl Command for MathSin { + fn name(&self) -> &str { + "math sin" + } + + fn signature(&self) -> Signature { + Signature::build("math sin") + .switch("degrees", "Use degrees instead of radians", Some('d')) + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the sine of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let use_degrees = call.has_flag(engine_state, stack, "degrees")?; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| operate(value, head, use_degrees), + engine_state.signals(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Apply the sine to Ï€/2", + example: "3.141592 / 2 | math sin | math round --precision 4", + result: Some(Value::test_float(1f64)), + }, + Example { + description: "Apply the sine to a list of angles in degrees", + example: "[0 90 180 270 360] | math sin -d | math round --precision 4", + result: Some(Value::list( + vec![ + Value::test_float(0f64), + Value::test_float(1f64), + Value::test_float(0f64), + Value::test_float(-1f64), + Value::test_float(0f64), + ], + Span::test_data(), + )), + }, + ] + } +} + +fn operate(value: Value, head: Span, use_degrees: bool) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + let val = if use_degrees { val.to_radians() } else { val }; + + Value::float(val.sin(), span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathSin {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/sinh.rs b/nushell/crates/nu-cmd-extra/src/extra/math/sinh.rs new file mode 100644 index 0000000..3ac7d8f --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/sinh.rs @@ -0,0 +1,92 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathSinH; + +impl Command for MathSinH { + fn name(&self) -> &str { + "math sinh" + } + + fn signature(&self) -> Signature { + Signature::build("math sinh") + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the hyperbolic sine of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "hyperbolic"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| operate(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + let e = std::f64::consts::E; + vec![Example { + description: "Apply the hyperbolic sine to 1", + example: "1 | math sinh", + result: Some(Value::test_float((e * e - 1.0) / (2.0 * e))), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + Value::float(val.sinh(), span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathSinH {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/tan.rs b/nushell/crates/nu-cmd-extra/src/extra/math/tan.rs new file mode 100644 index 0000000..81a81aa --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/tan.rs @@ -0,0 +1,111 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathTan; + +impl Command for MathTan { + fn name(&self) -> &str { + "math tan" + } + + fn signature(&self) -> Signature { + Signature::build("math tan") + .switch("degrees", "Use degrees instead of radians", Some('d')) + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the tangent of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let use_degrees = call.has_flag(engine_state, stack, "degrees")?; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| operate(value, head, use_degrees), + engine_state.signals(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Apply the tangent to Ï€/4", + example: "3.141592 / 4 | math tan | math round --precision 4", + result: Some(Value::test_float(1f64)), + }, + Example { + description: "Apply the tangent to a list of angles in degrees", + example: "[-45 0 45] | math tan --degrees", + result: Some(Value::list( + vec![ + Value::test_float(-1f64), + Value::test_float(0f64), + Value::test_float(1f64), + ], + Span::test_data(), + )), + }, + ] + } +} + +fn operate(value: Value, head: Span, use_degrees: bool) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + let val = if use_degrees { val.to_radians() } else { val }; + + Value::float(val.tan(), span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathTan {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/math/tanh.rs b/nushell/crates/nu-cmd-extra/src/extra/math/tanh.rs new file mode 100644 index 0000000..1a09737 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/math/tanh.rs @@ -0,0 +1,91 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct MathTanH; + +impl Command for MathTanH { + fn name(&self) -> &str { + "math tanh" + } + + fn signature(&self) -> Signature { + Signature::build("math tanh") + .input_output_types(vec![ + (Type::Number, Type::Float), + ( + Type::List(Box::new(Type::Number)), + Type::List(Box::new(Type::Float)), + ), + ]) + .allow_variants_without_examples(true) + .category(Category::Math) + } + + fn description(&self) -> &str { + "Returns the hyperbolic tangent of the number." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["trigonometry", "hyperbolic"] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| operate(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Apply the hyperbolic tangent to 10*Ï€", + example: "3.141592 * 10 | math tanh | math round --precision 4", + result: Some(Value::test_float(1f64)), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + numeric @ (Value::Int { .. } | Value::Float { .. }) => { + let span = numeric.span(); + let (val, span) = match numeric { + Value::Int { val, .. } => (val as f64, span), + Value::Float { val, .. } => (val, span), + _ => unreachable!(), + }; + + Value::float(val.tanh(), span) + } + Value::Error { .. } => value, + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "numeric".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(MathTanH {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/mod.rs new file mode 100644 index 0000000..4dfcbdc --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/mod.rs @@ -0,0 +1,96 @@ +mod bits; +mod filters; +mod formats; +mod math; +mod platform; +mod strings; + +pub use bits::{Bits, BitsAnd, BitsNot, BitsOr, BitsRol, BitsRor, BitsShl, BitsShr, BitsXor}; +pub use formats::ToHtml; +pub use math::{MathArcCos, MathArcCosH, MathArcSin, MathArcSinH, MathArcTan, MathArcTanH}; +pub use math::{MathCos, MathCosH, MathSin, MathSinH, MathTan, MathTanH}; +pub use math::{MathExp, MathLn}; + +use nu_protocol::engine::{EngineState, StateWorkingSet}; + +pub fn add_extra_command_context(mut engine_state: EngineState) -> EngineState { + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + + macro_rules! bind_command { + ( $command:expr ) => { + working_set.add_decl(Box::new($command)); + }; + ( $( $command:expr ),* ) => { + $( working_set.add_decl(Box::new($command)); )* + }; + } + + bind_command!( + filters::UpdateCells, + filters::EachWhile, + filters::Roll, + filters::RollDown, + filters::RollUp, + filters::RollLeft, + filters::RollRight, + filters::Rotate + ); + + bind_command!(platform::ansi::Gradient); + + bind_command!( + strings::format::FormatPattern, + strings::format::FormatBits, + strings::format::FormatNumber, + strings::str_::case::Str, + strings::str_::case::StrCamelCase, + strings::str_::case::StrKebabCase, + strings::str_::case::StrPascalCase, + strings::str_::case::StrScreamingSnakeCase, + strings::str_::case::StrSnakeCase, + strings::str_::case::StrTitleCase + ); + + bind_command!(ToHtml, formats::FromUrl); + + // Bits + bind_command! { + Bits, + BitsAnd, + BitsNot, + BitsOr, + BitsRol, + BitsRor, + BitsShl, + BitsShr, + BitsXor + } + + // Math + bind_command! { + MathArcSin, + MathArcCos, + MathArcTan, + MathArcSinH, + MathArcCosH, + MathArcTanH, + MathSin, + MathCos, + MathTan, + MathSinH, + MathCosH, + MathTanH, + MathExp, + MathLn + }; + + working_set.render() + }; + + if let Err(err) = engine_state.merge_delta(delta) { + eprintln!("Error creating extra command context: {err:?}"); + } + + engine_state +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs b/nushell/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs new file mode 100644 index 0000000..2db772a --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs @@ -0,0 +1,328 @@ +use nu_ansi_term::{Gradient, Rgb, build_all_gradient_text, gradient::TargetGround}; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "ansi gradient" + } + + fn signature(&self) -> Signature { + Signature::build("ansi gradient") + .named( + "fgstart", + SyntaxShape::String, + "foreground gradient start color in hex (0x123456)", + Some('a'), + ) + .named( + "fgend", + SyntaxShape::String, + "foreground gradient end color in hex", + Some('b'), + ) + .named( + "bgstart", + SyntaxShape::String, + "background gradient start color in hex", + Some('c'), + ) + .named( + "bgend", + SyntaxShape::String, + "background gradient end color in hex", + Some('d'), + ) + .rest( + "cell path", + SyntaxShape::CellPath, + "For a data structure input, add a gradient to strings at the given cell paths.", + ) + .input_output_types(vec![ + (Type::String, Type::String), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ]) + .allow_variants_without_examples(true) + .category(Category::Platform) + } + + fn description(&self) -> &str { + "Add a color gradient (using ANSI color codes) to the given string." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "draw text in a gradient with foreground start and end colors", + example: "'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart '0x40c9ff' --fgend '0xe81cff'", + result: None, + }, + Example { + description: "draw text in a gradient with foreground start and end colors and background start and end colors", + example: "'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart '0x40c9ff' --fgend '0xe81cff' --bgstart '0xe81cff' --bgend '0x40c9ff'", + result: None, + }, + Example { + description: "draw text in a gradient by specifying foreground start color - end color is assumed to be black", + example: "'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart '0x40c9ff'", + result: None, + }, + Example { + description: "draw text in a gradient by specifying foreground end color - start color is assumed to be black", + example: "'Hello, Nushell! This is a gradient.' | ansi gradient --fgend '0xe81cff'", + result: None, + }, + ] + } +} + +fn value_to_color(v: Option) -> Result, ShellError> { + let s = match v { + None => return Ok(None), + Some(x) => x.coerce_into_string()?, + }; + Ok(Some(Rgb::from_hex_string(s))) +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let fgstart: Option = call.get_flag(engine_state, stack, "fgstart")?; + let fgend: Option = call.get_flag(engine_state, stack, "fgend")?; + let bgstart: Option = call.get_flag(engine_state, stack, "bgstart")?; + let bgend: Option = call.get_flag(engine_state, stack, "bgend")?; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + let fgs_hex = value_to_color(fgstart)?; + let fge_hex = value_to_color(fgend)?; + let bgs_hex = value_to_color(bgstart)?; + let bge_hex = value_to_color(bgend)?; + let head = call.head; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, fgs_hex, fge_hex, bgs_hex, bge_hex, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, fgs_hex, fge_hex, bgs_hex, bge_hex, head)), + ); + if let Err(error) = r { + return Value::error(error, head); + } + } + ret + } + }, + engine_state.signals(), + ) +} + +fn action( + input: &Value, + fg_start: Option, + fg_end: Option, + bg_start: Option, + bg_end: Option, + command_span: Span, +) -> Value { + let span = input.span(); + match input { + Value::String { val, .. } => { + match (fg_start, fg_end, bg_start, bg_end) { + (None, None, None, None) => { + // Error - no colors + Value::error( + ShellError::MissingParameter { + param_name: + "please supply foreground and/or background color parameters".into(), + span: command_span, + }, + span, + ) + } + (None, None, None, Some(bg_end)) => { + // Error - missing bg_start, so assume black + let bg_start = Rgb::new(0, 0, 0); + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(val, TargetGround::Background); + Value::string(gradient_string, span) + } + (None, None, Some(bg_start), None) => { + // Error - missing bg_end, so assume black + let bg_end = Rgb::new(0, 0, 0); + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(val, TargetGround::Background); + Value::string(gradient_string, span) + } + (None, None, Some(bg_start), Some(bg_end)) => { + // Background Only + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(val, TargetGround::Background); + Value::string(gradient_string, span) + } + (None, Some(fg_end), None, None) => { + // Error - missing fg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(val, TargetGround::Foreground); + Value::string(gradient_string, span) + } + (None, Some(fg_end), None, Some(bg_end)) => { + // missing fg_start and bg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + (None, Some(fg_end), Some(bg_start), None) => { + // Error - missing fg_start and bg_end + let fg_start = Rgb::new(0, 0, 0); + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + (None, Some(fg_end), Some(bg_start), Some(bg_end)) => { + // Error - missing fg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + (Some(fg_start), None, None, None) => { + // Error - missing fg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(val, TargetGround::Foreground); + Value::string(gradient_string, span) + } + (Some(fg_start), None, None, Some(bg_end)) => { + // Error - missing fg_end, bg_start, so assume black + let fg_end = Rgb::new(0, 0, 0); + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + (Some(fg_start), None, Some(bg_start), None) => { + // Error - missing fg_end, bg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + (Some(fg_start), None, Some(bg_start), Some(bg_end)) => { + // Error - missing fg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + (Some(fg_start), Some(fg_end), None, None) => { + // Foreground Only + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(val, TargetGround::Foreground); + Value::string(gradient_string, span) + } + (Some(fg_start), Some(fg_end), None, Some(bg_end)) => { + // Error - missing bg_start, so assume black + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + (Some(fg_start), Some(fg_end), Some(bg_start), None) => { + // Error - missing bg_end, so assume black + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + (Some(fg_start), Some(fg_end), Some(bg_start), Some(bg_end)) => { + // Foreground and Background Gradient + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, span) + } + } + } + other => { + let got = format!("value is {}, not string", other.get_type()); + + Value::error( + ShellError::TypeMismatch { + err_message: got, + span: other.span(), + }, + other.span(), + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::{SubCommand, action}; + use nu_ansi_term::Rgb; + use nu_protocol::{Span, Value}; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn test_fg_gradient() { + let input_string = Value::test_string("Hello, World!"); + let expected = Value::test_string( + "\u{1b}[38;2;64;201;255mH\u{1b}[38;2;76;187;254me\u{1b}[38;2;89;174;254ml\u{1b}[38;2;102;160;254ml\u{1b}[38;2;115;147;254mo\u{1b}[38;2;128;133;254m,\u{1b}[38;2;141;120;254m \u{1b}[38;2;153;107;254mW\u{1b}[38;2;166;94;254mo\u{1b}[38;2;179;80;254mr\u{1b}[38;2;192;67;254ml\u{1b}[38;2;205;53;254md\u{1b}[38;2;218;40;254m!\u{1b}[0m", + ); + let fg_start = Rgb::from_hex_string("0x40c9ff".to_string()); + let fg_end = Rgb::from_hex_string("0xe81cff".to_string()); + let actual = action( + &input_string, + Some(fg_start), + Some(fg_end), + None, + None, + Span::test_data(), + ); + assert_eq!(actual, expected); + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/platform/ansi/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/platform/ansi/mod.rs new file mode 100644 index 0000000..036fa1e --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/platform/ansi/mod.rs @@ -0,0 +1,3 @@ +mod gradient; + +pub(crate) use gradient::SubCommand as Gradient; diff --git a/nushell/crates/nu-cmd-extra/src/extra/platform/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/platform/mod.rs new file mode 100644 index 0000000..123734c --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/platform/mod.rs @@ -0,0 +1 @@ +pub(crate) mod ansi; diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/encode_decode/decode_hex.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/encode_decode/decode_hex.rs new file mode 100644 index 0000000..f846afe --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/encode_decode/decode_hex.rs @@ -0,0 +1,76 @@ +use super::hex::{operate, ActionType}; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct DecodeHex; + +impl Command for DecodeHex { + fn name(&self) -> &str { + "decode hex" + } + + fn signature(&self) -> Signature { + Signature::build("decode hex") + .input_output_types(vec![ + (Type::String, Type::Binary), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::Binary)), + ), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, decode data at the given cell paths", + ) + .category(Category::Formats) + } + + fn description(&self) -> &str { + "Hex decode a value." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Hex decode a value and output as binary", + example: "'0102030A0a0B' | decode hex", + result: Some(Value::binary( + [0x01, 0x02, 0x03, 0x0A, 0x0A, 0x0B], + Span::test_data(), + )), + }, + Example { + description: "Whitespaces are allowed to be between hex digits", + example: "'01 02 03 0A 0a 0B' | decode hex", + result: Some(Value::binary( + [0x01, 0x02, 0x03, 0x0A, 0x0A, 0x0B], + Span::test_data(), + )), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(ActionType::Decode, engine_state, stack, call, input) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(DecodeHex) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/encode_decode/encode_hex.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/encode_decode/encode_hex.rs new file mode 100644 index 0000000..c3a5467 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/encode_decode/encode_hex.rs @@ -0,0 +1,63 @@ +use super::hex::{operate, ActionType}; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct EncodeHex; + +impl Command for EncodeHex { + fn name(&self) -> &str { + "encode hex" + } + + fn signature(&self) -> Signature { + Signature::build("encode hex") + .input_output_types(vec![ + (Type::Binary, Type::String), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::String)), + ), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, encode data at the given cell paths", + ) + .category(Category::Formats) + } + + fn description(&self) -> &str { + "Encode a binary value using hex." + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Encode binary data", + example: "0x[09 F9 11 02 9D 74 E3 5B D8 41 56 C5 63 56 88 C0] | encode hex", + result: Some(Value::test_string("09F911029D74E35BD84156C5635688C0")), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(ActionType::Encode, engine_state, stack, call, input) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(EncodeHex) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/format/bits.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/format/bits.rs new file mode 100644 index 0000000..f8d9309 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/format/bits.rs @@ -0,0 +1,239 @@ +use std::io::{self, Read, Write}; + +use nu_cmd_base::input_handler::{CmdArgument, operate}; +use nu_engine::command_prelude::*; + +use nu_protocol::{Signals, shell_error::io::IoError}; +use num_traits::ToPrimitive; + +struct Arguments { + cell_paths: Option>, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + self.cell_paths.take() + } +} + +#[derive(Clone)] +pub struct FormatBits; + +impl Command for FormatBits { + fn name(&self) -> &str { + "format bits" + } + + fn signature(&self) -> Signature { + Signature::build("format bits") + .input_output_types(vec![ + (Type::Binary, Type::String), + (Type::Int, Type::String), + (Type::Filesize, Type::String), + (Type::Duration, Type::String), + (Type::String, Type::String), + (Type::Bool, Type::String), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ]) + .allow_variants_without_examples(true) // TODO: supply exhaustive examples + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, convert data at the given cell paths.", + ) + .category(Category::Conversions) + } + + fn description(&self) -> &str { + "Convert value to a string of binary data represented by 0 and 1." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "cast", "binary"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + format_bits(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a binary value into a string, padded to 8 places with 0s", + example: "0x[1] | format bits", + result: Some(Value::string("00000001", Span::test_data())), + }, + Example { + description: "convert an int into a string, padded to 8 places with 0s", + example: "1 | format bits", + result: Some(Value::string("00000001", Span::test_data())), + }, + Example { + description: "convert a filesize value into a string, padded to 8 places with 0s", + example: "1b | format bits", + result: Some(Value::string("00000001", Span::test_data())), + }, + Example { + description: "convert a duration value into a string, padded to 8 places with 0s", + example: "1ns | format bits", + result: Some(Value::string("00000001", Span::test_data())), + }, + Example { + description: "convert a boolean value into a string, padded to 8 places with 0s", + example: "true | format bits", + result: Some(Value::string("00000001", Span::test_data())), + }, + Example { + description: "convert a string into a raw binary string, padded with 0s to 8 places", + example: "'nushell.sh' | format bits", + result: Some(Value::string( + "01101110 01110101 01110011 01101000 01100101 01101100 01101100 00101110 01110011 01101000", + Span::test_data(), + )), + }, + ] + } +} + +fn format_bits( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let cell_paths = call.rest(engine_state, stack, 0)?; + let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); + + if let PipelineData::ByteStream(stream, metadata) = input { + Ok(PipelineData::ByteStream( + byte_stream_to_bits(stream, head), + metadata, + )) + } else { + let args = Arguments { cell_paths }; + operate(action, args, input, call.head, engine_state.signals()) + } +} + +fn byte_stream_to_bits(stream: ByteStream, head: Span) -> ByteStream { + if let Some(mut reader) = stream.reader() { + let mut is_first = true; + ByteStream::from_fn( + head, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + let mut byte = [0]; + if reader + .read(&mut byte[..]) + .map_err(|err| IoError::new(err, head, None))? + > 0 + { + // Format the byte as bits + if is_first { + is_first = false; + } else { + buffer.push(b' '); + } + write!(buffer, "{:08b}", byte[0]).expect("format failed"); + Ok(true) + } else { + // EOF + Ok(false) + } + }, + ) + } else { + ByteStream::read(io::empty(), head, Signals::empty(), ByteStreamType::String) + } +} + +fn convert_to_smallest_number_type(num: i64, span: Span) -> Value { + if let Some(v) = num.to_i8() { + let bytes = v.to_ne_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::string(raw_string.trim(), span) + } else if let Some(v) = num.to_i16() { + let bytes = v.to_ne_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::string(raw_string.trim(), span) + } else if let Some(v) = num.to_i32() { + let bytes = v.to_ne_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::string(raw_string.trim(), span) + } else { + let bytes = num.to_ne_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::string(raw_string.trim(), span) + } +} + +fn action(input: &Value, _args: &Arguments, span: Span) -> Value { + match input { + Value::Binary { val, .. } => { + let mut raw_string = "".to_string(); + for ch in val { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::string(raw_string.trim(), span) + } + Value::Int { val, .. } => convert_to_smallest_number_type(*val, span), + Value::Filesize { val, .. } => convert_to_smallest_number_type(val.get(), span), + Value::Duration { val, .. } => convert_to_smallest_number_type(*val, span), + Value::String { val, .. } => { + let raw_bytes = val.as_bytes(); + let mut raw_string = "".to_string(); + for ch in raw_bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::string(raw_string.trim(), span) + } + Value::Bool { val, .. } => { + let v = >::from(*val); + convert_to_smallest_number_type(v, span) + } + // Propagate errors by explicitly matching them before the final case. + Value::Error { .. } => input.clone(), + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "int, filesize, string, duration, binary, or bool".into(), + wrong_type: other.get_type().to_string(), + dst_span: span, + src_span: other.span(), + }, + span, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FormatBits {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/format/command.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/format/command.rs new file mode 100644 index 0000000..9e3adaf --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/format/command.rs @@ -0,0 +1,276 @@ +use nu_engine::command_prelude::*; +use nu_protocol::{Config, ListStream, ast::PathMember, casing::Casing, engine::StateWorkingSet}; + +#[derive(Clone)] +pub struct FormatPattern; + +impl Command for FormatPattern { + fn name(&self) -> &str { + "format pattern" + } + + fn signature(&self) -> Signature { + Signature::build("format pattern") + .input_output_types(vec![ + (Type::table(), Type::List(Box::new(Type::String))), + (Type::record(), Type::Any), + ]) + .required( + "pattern", + SyntaxShape::String, + "The pattern to output. e.g.) \"{foo}: {bar}\".", + ) + .allow_variants_without_examples(true) + .category(Category::Strings) + } + + fn description(&self) -> &str { + "Format columns into a string using a simple pattern." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let mut working_set = StateWorkingSet::new(engine_state); + + let specified_pattern: Result = call.req(engine_state, stack, 0); + let input_val = input.into_value(call.head)?; + // add '$it' variable to support format like this: $it.column1.column2. + let it_id = working_set.add_variable(b"$it".to_vec(), call.head, Type::Any, false); + stack.add_var(it_id, input_val.clone()); + + let config = stack.get_config(engine_state); + + match specified_pattern { + Err(e) => Err(e), + Ok(pattern) => { + let string_span = pattern.span(); + let string_pattern = pattern.coerce_into_string()?; + // the string span is start as `"`, we don't need the character + // to generate proper span for sub expression. + let ops = extract_formatting_operations( + string_pattern, + call.head, + string_span.start + 1, + )?; + + format(input_val, &ops, engine_state, &config, call.head) + } + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Print filenames with their sizes", + example: "ls | format pattern '{name}: {size}'", + result: None, + }, + Example { + description: "Print elements from some columns of a table", + example: "[[col1, col2]; [v1, v2] [v3, v4]] | format pattern '{col2}'", + result: Some(Value::list( + vec![Value::test_string("v2"), Value::test_string("v4")], + Span::test_data(), + )), + }, + ] + } +} + +// NOTE: The reason to split {column1.column2} and {$it.column1.column2}: +// for {column1.column2}, we just need to follow given record or list. +// for {$it.column1.column2} or {$variable}, we need to manually evaluate the expression. +// +// Have thought about converting from {column1.column2} to {$it.column1.column2}, but that +// will extend input relative span, finally make `nu` panic out with message: span missing in file +// contents cache. +#[derive(Debug)] +enum FormatOperation { + FixedText(String), + // raw input is something like {column1.column2} + ValueFromColumn(String, Span), +} + +/// Given a pattern that is fed into the Format command, we can process it and subdivide it +/// in two kind of operations. +/// FormatOperation::FixedText contains a portion of the pattern that has to be placed +/// there without any further processing. +/// FormatOperation::ValueFromColumn contains the name of a column whose values will be +/// formatted according to the input pattern. +/// "$it.column1.column2" or "$variable" +fn extract_formatting_operations( + input: String, + error_span: Span, + span_start: usize, +) -> Result, ShellError> { + let mut output = vec![]; + + let mut characters = input.char_indices(); + + let mut column_span_start = 0; + let mut column_span_end = 0; + loop { + let mut before_bracket = String::new(); + + for (index, ch) in &mut characters { + if ch == '{' { + column_span_start = index + 1; // not include '{' character. + break; + } + before_bracket.push(ch); + } + + if !before_bracket.is_empty() { + output.push(FormatOperation::FixedText(before_bracket.to_string())); + } + + let mut column_name = String::new(); + let mut column_need_eval = false; + for (index, ch) in &mut characters { + if ch == '$' { + column_need_eval = true; + } + + if ch == '}' { + column_span_end = index; // not include '}' character. + break; + } + column_name.push(ch); + } + + if column_span_end < column_span_start { + return Err(ShellError::DelimiterError { + msg: "there are unmatched curly braces".to_string(), + span: error_span, + }); + } + + if !column_name.is_empty() { + if column_need_eval { + return Err(ShellError::GenericError { + error: "Removed functionality".into(), + msg: "The ability to use variables ($it) in `format pattern` has been removed." + .into(), + span: Some(error_span), + help: Some( + "You can use other formatting options, such as string interpolation." + .into(), + ), + inner: vec![], + }); + } else { + output.push(FormatOperation::ValueFromColumn( + column_name.clone(), + Span::new(span_start + column_span_start, span_start + column_span_end), + )); + } + } + + if before_bracket.is_empty() && column_name.is_empty() { + break; + } + } + Ok(output) +} + +/// Format the incoming PipelineData according to the pattern +fn format( + input_data: Value, + format_operations: &[FormatOperation], + engine_state: &EngineState, + config: &Config, + head_span: Span, +) -> Result { + let data_as_value = input_data; + + // We can only handle a Record or a List of Records + match data_as_value { + Value::Record { .. } => match format_record(format_operations, &data_as_value, config) { + Ok(value) => Ok(PipelineData::Value(Value::string(value, head_span), None)), + Err(value) => Err(value), + }, + + Value::List { vals, .. } => { + let mut list = vec![]; + for val in vals.iter() { + match val { + Value::Record { .. } => match format_record(format_operations, val, config) { + Ok(value) => { + list.push(Value::string(value, head_span)); + } + Err(value) => { + return Err(value); + } + }, + Value::Error { error, .. } => return Err(*error.clone()), + _ => { + return Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "record".to_string(), + wrong_type: val.get_type().to_string(), + dst_span: head_span, + src_span: val.span(), + }); + } + } + } + + Ok(ListStream::new(list.into_iter(), head_span, engine_state.signals().clone()).into()) + } + // Unwrapping this ShellError is a bit unfortunate. + // Ideally, its Span would be preserved. + Value::Error { error, .. } => Err(*error), + _ => Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "record".to_string(), + wrong_type: data_as_value.get_type().to_string(), + dst_span: head_span, + src_span: data_as_value.span(), + }), + } +} + +fn format_record( + format_operations: &[FormatOperation], + data_as_value: &Value, + config: &Config, +) -> Result { + let mut output = String::new(); + + for op in format_operations { + match op { + FormatOperation::FixedText(s) => output.push_str(s.as_str()), + FormatOperation::ValueFromColumn(col_name, span) => { + // path member should split by '.' to handle for nested structure. + let path_members: Vec = col_name + .split('.') + .map(|path| PathMember::String { + val: path.to_string(), + span: *span, + optional: false, + casing: Casing::Sensitive, + }) + .collect(); + + let expanded_string = data_as_value + .follow_cell_path(&path_members)? + .to_expanded_string(", ", config); + output.push_str(expanded_string.as_str()) + } + } + } + Ok(output) +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::FormatPattern; + use crate::test_examples; + test_examples(FormatPattern {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/format/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/format/mod.rs new file mode 100644 index 0000000..f032474 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/format/mod.rs @@ -0,0 +1,7 @@ +mod bits; +mod command; +mod number; + +pub(crate) use bits::FormatBits; +pub(crate) use command::FormatPattern; +pub(crate) use number::FormatNumber; diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/format/number.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/format/number.rs new file mode 100644 index 0000000..7af6a6e --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/format/number.rs @@ -0,0 +1,204 @@ +use nu_cmd_base::input_handler::{CellPathOnlyArgs, operate}; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct FormatNumber; + +impl Command for FormatNumber { + fn name(&self) -> &str { + "format number" + } + + fn description(&self) -> &str { + "Format a number." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("format number") + .input_output_types(vec![(Type::Number, Type::record())]) + .switch( + "no-prefix", + "don't include the binary, hex or octal prefixes", + Some('n'), + ) + .category(Category::Conversions) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["display", "render", "fmt"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get a record containing multiple formats for the number 42", + example: "42 | format number", + result: Some(Value::test_record(record! { + "debug" => Value::test_string("42"), + "display" => Value::test_string("42"), + "binary" => Value::test_string("0b101010"), + "lowerexp" => Value::test_string("4.2e1"), + "upperexp" => Value::test_string("4.2E1"), + "lowerhex" => Value::test_string("0x2a"), + "upperhex" => Value::test_string("0x2A"), + "octal" => Value::test_string("0o52"), + })), + }, + Example { + description: "Format float without prefixes", + example: "3.14 | format number --no-prefix", + result: Some(Value::test_record(record! { + "debug" => Value::test_string("3.14"), + "display" => Value::test_string("3.14"), + "binary" => Value::test_string("100000000001001000111101011100001010001111010111000010100011111"), + "lowerexp" => Value::test_string("3.14e0"), + "upperexp" => Value::test_string("3.14E0"), + "lowerhex" => Value::test_string("40091eb851eb851f"), + "upperhex" => Value::test_string("40091EB851EB851F"), + "octal" => Value::test_string("400110753412172702437"), + })), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + format_number(engine_state, stack, call, input) + } +} + +pub(crate) fn format_number( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let cell_paths: Vec = call.rest(engine_state, stack, 0)?; + let args = CellPathOnlyArgs::from(cell_paths); + if call.has_flag(engine_state, stack, "no-prefix")? { + operate( + action_no_prefix, + args, + input, + call.head, + engine_state.signals(), + ) + } else { + operate(action, args, input, call.head, engine_state.signals()) + } +} + +fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { + match input { + Value::Float { val, .. } => format_f64(*val, false, span), + Value::Int { val, .. } => format_i64(*val, false, span), + Value::Filesize { val, .. } => format_i64(val.get(), false, span), + // Propagate errors by explicitly matching them before the final case. + Value::Error { .. } => input.clone(), + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "float, int, or filesize".into(), + wrong_type: other.get_type().to_string(), + dst_span: span, + src_span: other.span(), + }, + span, + ), + } +} + +fn action_no_prefix(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { + match input { + Value::Float { val, .. } => format_f64(*val, true, span), + Value::Int { val, .. } => format_i64(*val, true, span), + Value::Filesize { val, .. } => format_i64(val.get(), true, span), + // Propagate errors by explicitly matching them before the final case. + Value::Error { .. } => input.clone(), + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "float, int, or filesize".into(), + wrong_type: other.get_type().to_string(), + dst_span: span, + src_span: other.span(), + }, + span, + ), + } +} + +fn format_i64(num: i64, no_prefix: bool, span: Span) -> Value { + Value::record( + record! { + "debug" => Value::string(format!("{num:#?}"), span), + "display" => Value::string(format!("{num}"), span), + "binary" => Value::string( + if no_prefix { format!("{num:b}") } else { format!("{num:#b}") }, + span, + ), + "lowerexp" => Value::string(format!("{num:#e}"), span), + "upperexp" => Value::string(format!("{num:#E}"), span), + "lowerhex" => Value::string( + if no_prefix { format!("{num:x}") } else { format!("{num:#x}") }, + span, + ), + "upperhex" => Value::string( + if no_prefix { format!("{num:X}") } else { format!("{num:#X}") }, + span, + ), + "octal" => Value::string( + if no_prefix { format!("{num:o}") } else { format!("{num:#o}") }, + span, + ) + }, + span, + ) +} + +fn format_f64(num: f64, no_prefix: bool, span: Span) -> Value { + Value::record( + record! { + "debug" => Value::string(format!("{num:#?}"), span), + "display" => Value::string(format!("{num}"), span), + "binary" => Value::string( + if no_prefix { + format!("{:b}", num.to_bits()) + } else { + format!("{:#b}", num.to_bits()) + }, + span, + ), + "lowerexp" => Value::string(format!("{num:#e}"), span), + "upperexp" => Value::string(format!("{num:#E}"), span), + "lowerhex" => Value::string( + if no_prefix { format!("{:x}", num.to_bits()) } else { format!("{:#x}", num.to_bits()) }, + span, + ), + "upperhex" => Value::string( + if no_prefix { format!("{:X}", num.to_bits()) } else { format!("{:#X}", num.to_bits()) }, + span, + ), + "octal" => Value::string( + if no_prefix { format!("{:o}", num.to_bits()) } else { format!("{:#o}", num.to_bits()) }, + span, + ) + }, + span, + ) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FormatNumber {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/mod.rs new file mode 100644 index 0000000..8d43cd8 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod format; +pub(crate) mod str_; diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/camel_case.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/camel_case.rs new file mode 100644 index 0000000..a2809bd --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/camel_case.rs @@ -0,0 +1,96 @@ +use super::operate; +use heck::ToLowerCamelCase; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct StrCamelCase; + +impl Command for StrCamelCase { + fn name(&self) -> &str { + "str camel-case" + } + + fn signature(&self) -> Signature { + Signature::build("str camel-case") + .input_output_types(vec![ + (Type::String, Type::String), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, convert strings at the given cell paths.", + ) + .category(Category::Strings) + } + + fn description(&self) -> &str { + "Convert a string to camelCase." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "style", "caps", "convention"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate( + engine_state, + stack, + call, + input, + &ToLowerCamelCase::to_lower_camel_case, + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to camelCase", + example: " 'NuShell' | str camel-case", + result: Some(Value::test_string("nuShell")), + }, + Example { + description: "convert a string to camelCase", + example: "'this-is-the-first-case' | str camel-case", + result: Some(Value::test_string("thisIsTheFirstCase")), + }, + Example { + description: "convert a string to camelCase", + example: " 'this_is_the_second_case' | str camel-case", + result: Some(Value::test_string("thisIsTheSecondCase")), + }, + Example { + description: "convert a column from a table to camelCase", + example: r#"[[lang, gems]; [nu_test, 100]] | str camel-case lang"#, + result: Some(Value::test_list(vec![Value::test_record(record! { + "lang" => Value::test_string("nuTest"), + "gems" => Value::test_int(100), + })])), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(StrCamelCase {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/kebab_case.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/kebab_case.rs new file mode 100644 index 0000000..61ba7d8 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/kebab_case.rs @@ -0,0 +1,95 @@ +use super::operate; +use heck::ToKebabCase; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct StrKebabCase; + +impl Command for StrKebabCase { + fn name(&self) -> &str { + "str kebab-case" + } + + fn signature(&self) -> Signature { + Signature::build("str kebab-case") + .input_output_types(vec![ + (Type::String, Type::String), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, convert strings at the given cell paths.", + ) + .category(Category::Strings) + } + + fn description(&self) -> &str { + "Convert a string to kebab-case." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "style", "hyphens", "convention"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate( + engine_state, + stack, + call, + input, + &ToKebabCase::to_kebab_case, + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to kebab-case", + example: "'NuShell' | str kebab-case", + result: Some(Value::test_string("nu-shell")), + }, + Example { + description: "convert a string to kebab-case", + example: "'thisIsTheFirstCase' | str kebab-case", + result: Some(Value::test_string("this-is-the-first-case")), + }, + Example { + description: "convert a string to kebab-case", + example: "'THIS_IS_THE_SECOND_CASE' | str kebab-case", + result: Some(Value::test_string("this-is-the-second-case")), + }, + Example { + description: "convert a column from a table to kebab-case", + example: r#"[[lang, gems]; [nuTest, 100]] | str kebab-case lang"#, + result: Some(Value::test_list(vec![Value::test_record(record! { + "lang" => Value::test_string("nu-test"), + "gems" => Value::test_int(100), + })])), + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(StrKebabCase {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs new file mode 100644 index 0000000..d782856 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs @@ -0,0 +1,68 @@ +mod camel_case; +mod kebab_case; +mod pascal_case; +mod screaming_snake_case; +mod snake_case; +mod str_; +mod title_case; + +pub use camel_case::StrCamelCase; +pub use kebab_case::StrKebabCase; +pub use pascal_case::StrPascalCase; +pub use screaming_snake_case::StrScreamingSnakeCase; +pub use snake_case::StrSnakeCase; +pub use str_::Str; +pub use title_case::StrTitleCase; + +use nu_cmd_base::input_handler::{CmdArgument, operate as general_operate}; +use nu_engine::command_prelude::*; + +struct Arguments String + Send + Sync + 'static> { + case_operation: &'static F, + cell_paths: Option>, +} + +impl String + Send + Sync + 'static> CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + self.cell_paths.take() + } +} + +pub fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + case_operation: &'static F, +) -> Result +where + F: Fn(&str) -> String + Send + Sync + 'static, +{ + let cell_paths: Vec = call.rest(engine_state, stack, 0)?; + let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); + let args = Arguments { + case_operation, + cell_paths, + }; + general_operate(action, args, input, call.head, engine_state.signals()) +} + +fn action(input: &Value, args: &Arguments, head: Span) -> Value +where + F: Fn(&str) -> String + Send + Sync + 'static, +{ + let case_operation = args.case_operation; + match input { + Value::String { val, .. } => Value::string(case_operation(val), head), + Value::Error { .. } => input.clone(), + _ => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "string".into(), + wrong_type: input.get_type().to_string(), + dst_span: head, + src_span: input.span(), + }, + head, + ), + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/pascal_case.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/pascal_case.rs new file mode 100644 index 0000000..ff15cee --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/pascal_case.rs @@ -0,0 +1,96 @@ +use super::operate; +use heck::ToUpperCamelCase; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct StrPascalCase; + +impl Command for StrPascalCase { + fn name(&self) -> &str { + "str pascal-case" + } + + fn signature(&self) -> Signature { + Signature::build("str pascal-case") + .input_output_types(vec![ + (Type::String, Type::String), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, convert strings at the given cell paths.", + ) + .category(Category::Strings) + } + + fn description(&self) -> &str { + "Convert a string to PascalCase." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "style", "caps", "upper", "convention"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate( + engine_state, + stack, + call, + input, + &ToUpperCamelCase::to_upper_camel_case, + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to PascalCase", + example: "'nu-shell' | str pascal-case", + result: Some(Value::test_string("NuShell")), + }, + Example { + description: "convert a string to PascalCase", + example: "'this-is-the-first-case' | str pascal-case", + result: Some(Value::test_string("ThisIsTheFirstCase")), + }, + Example { + description: "convert a string to PascalCase", + example: "'this_is_the_second_case' | str pascal-case", + result: Some(Value::test_string("ThisIsTheSecondCase")), + }, + Example { + description: "convert a column from a table to PascalCase", + example: r#"[[lang, gems]; [nu_test, 100]] | str pascal-case lang"#, + result: Some(Value::test_list(vec![Value::test_record(record! { + "lang" => Value::test_string("NuTest"), + "gems" => Value::test_int(100), + })])), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(StrPascalCase {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/screaming_snake_case.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/screaming_snake_case.rs new file mode 100644 index 0000000..ef55254 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/screaming_snake_case.rs @@ -0,0 +1,96 @@ +use super::operate; +use heck::ToShoutySnakeCase; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct StrScreamingSnakeCase; + +impl Command for StrScreamingSnakeCase { + fn name(&self) -> &str { + "str screaming-snake-case" + } + + fn signature(&self) -> Signature { + Signature::build("str screaming-snake-case") + .input_output_types(vec![ + (Type::String, Type::String), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, convert strings at the given cell paths.", + ) + .category(Category::Strings) + } + + fn description(&self) -> &str { + "Convert a string to SCREAMING_SNAKE_CASE." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "style", "underscore", "convention"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate( + engine_state, + stack, + call, + input, + &ToShoutySnakeCase::to_shouty_snake_case, + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to SCREAMING_SNAKE_CASE", + example: r#" "NuShell" | str screaming-snake-case"#, + result: Some(Value::test_string("NU_SHELL")), + }, + Example { + description: "convert a string to SCREAMING_SNAKE_CASE", + example: r#" "this_is_the_second_case" | str screaming-snake-case"#, + result: Some(Value::test_string("THIS_IS_THE_SECOND_CASE")), + }, + Example { + description: "convert a string to SCREAMING_SNAKE_CASE", + example: r#""this-is-the-first-case" | str screaming-snake-case"#, + result: Some(Value::test_string("THIS_IS_THE_FIRST_CASE")), + }, + Example { + description: "convert a column from a table to SCREAMING_SNAKE_CASE", + example: r#"[[lang, gems]; [nu_test, 100]] | str screaming-snake-case lang"#, + result: Some(Value::test_list(vec![Value::test_record(record! { + "lang" => Value::test_string("NU_TEST"), + "gems" => Value::test_int(100), + })])), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(StrScreamingSnakeCase {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/snake_case.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/snake_case.rs new file mode 100644 index 0000000..a3913eb --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/snake_case.rs @@ -0,0 +1,96 @@ +use super::operate; +use heck::ToSnakeCase; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct StrSnakeCase; + +impl Command for StrSnakeCase { + fn name(&self) -> &str { + "str snake-case" + } + + fn signature(&self) -> Signature { + Signature::build("str snake-case") + .input_output_types(vec![ + (Type::String, Type::String), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, convert strings at the given cell paths.", + ) + .category(Category::Strings) + } + + fn description(&self) -> &str { + "Convert a string to snake_case." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "style", "underscore", "lower", "convention"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate( + engine_state, + stack, + call, + input, + &ToSnakeCase::to_snake_case, + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to snake_case", + example: r#" "NuShell" | str snake-case"#, + result: Some(Value::test_string("nu_shell")), + }, + Example { + description: "convert a string to snake_case", + example: r#" "this_is_the_second_case" | str snake-case"#, + result: Some(Value::test_string("this_is_the_second_case")), + }, + Example { + description: "convert a string to snake_case", + example: r#""this-is-the-first-case" | str snake-case"#, + result: Some(Value::test_string("this_is_the_first_case")), + }, + Example { + description: "convert a column from a table to snake_case", + example: r#"[[lang, gems]; [nuTest, 100]] | str snake-case lang"#, + result: Some(Value::test_list(vec![Value::test_record(record! { + "lang" => Value::test_string("nu_test"), + "gems" => Value::test_int(100), + })])), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(StrSnakeCase {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/str_.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/str_.rs new file mode 100644 index 0000000..4ee04ec --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/str_.rs @@ -0,0 +1,34 @@ +use nu_engine::{command_prelude::*, get_full_help}; + +#[derive(Clone)] +pub struct Str; + +impl Command for Str { + fn name(&self) -> &str { + "str" + } + + fn signature(&self) -> Signature { + Signature::build("str") + .category(Category::Strings) + .input_output_types(vec![(Type::Nothing, Type::String)]) + } + + fn description(&self) -> &str { + "Various commands for working with string data." + } + + fn extra_description(&self) -> &str { + "You must use one of the following subcommands. Using this command as-is will only produce this help message." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/title_case.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/title_case.rs new file mode 100644 index 0000000..7f83059 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/case/title_case.rs @@ -0,0 +1,91 @@ +use super::operate; +use heck::ToTitleCase; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct StrTitleCase; + +impl Command for StrTitleCase { + fn name(&self) -> &str { + "str title-case" + } + + fn signature(&self) -> Signature { + Signature::build("str title-case") + .input_output_types(vec![ + (Type::String, Type::String), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + (Type::table(), Type::table()), + (Type::record(), Type::record()), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, convert strings at the given cell paths.", + ) + .category(Category::Strings) + } + + fn description(&self) -> &str { + "Convert a string to Title Case." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "style", "convention"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate( + engine_state, + stack, + call, + input, + &ToTitleCase::to_title_case, + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to Title Case", + example: "'nu-shell' | str title-case", + result: Some(Value::test_string("Nu Shell")), + }, + Example { + description: "convert a string to Title Case", + example: "'this is a test case' | str title-case", + result: Some(Value::test_string("This Is A Test Case")), + }, + Example { + description: "convert a column from a table to Title Case", + example: r#"[[title, count]; ['nu test', 100]] | str title-case title"#, + result: Some(Value::test_list(vec![Value::test_record(record! { + "title" => Value::test_string("Nu Test"), + "count" => Value::test_int(100), + })])), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(StrTitleCase {}) + } +} diff --git a/nushell/crates/nu-cmd-extra/src/extra/strings/str_/mod.rs b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/mod.rs new file mode 100644 index 0000000..481b440 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/extra/strings/str_/mod.rs @@ -0,0 +1 @@ +pub(crate) mod case; diff --git a/nushell/crates/nu-cmd-extra/src/lib.rs b/nushell/crates/nu-cmd-extra/src/lib.rs new file mode 100644 index 0000000..f6f97c2 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/src/lib.rs @@ -0,0 +1,7 @@ +#![doc = include_str!("../README.md")] +mod example_test; +pub mod extra; +pub use extra::*; + +#[cfg(test)] +pub use example_test::test_examples; diff --git a/nushell/crates/nu-cmd-extra/tests/commands/bits/format.rs b/nushell/crates/nu-cmd-extra/tests/commands/bits/format.rs new file mode 100644 index 0000000..a06b6d8 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/tests/commands/bits/format.rs @@ -0,0 +1,13 @@ +use nu_test_support::nu; + +#[test] +fn byte_stream_into_bits() { + let result = nu!("[0x[01] 0x[02 03]] | bytes collect | format bits"); + assert_eq!("00000001 00000010 00000011", result.out); +} + +#[test] +fn byte_stream_into_bits_is_stream() { + let result = nu!("[0x[01] 0x[02 03]] | bytes collect | format bits | describe"); + assert_eq!("string (stream)", result.out); +} diff --git a/nushell/crates/nu-cmd-extra/tests/commands/bits/mod.rs b/nushell/crates/nu-cmd-extra/tests/commands/bits/mod.rs new file mode 100644 index 0000000..8631268 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/tests/commands/bits/mod.rs @@ -0,0 +1 @@ +mod format; diff --git a/nushell/crates/nu-cmd-extra/tests/commands/bytes/ends_with.rs b/nushell/crates/nu-cmd-extra/tests/commands/bytes/ends_with.rs new file mode 100644 index 0000000..b90f936 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/tests/commands/bytes/ends_with.rs @@ -0,0 +1,120 @@ +use nu_test_support::nu; + +#[test] +fn basic_binary_end_with() { + let actual = nu!(r#" + "hello world" | into binary | bytes ends-with 0x[77 6f 72 6c 64] + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn basic_string_fails() { + let actual = nu!(r#" + "hello world" | bytes ends-with 0x[77 6f 72 6c 64] + "#); + + assert!(actual.err.contains("command doesn't support")); + assert_eq!(actual.out, ""); +} + +#[test] +fn short_stream_binary() { + let actual = nu!(r#" + nu --testbin repeater (0x[01]) 5 | bytes ends-with 0x[010101] + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn short_stream_mismatch() { + let actual = nu!(r#" + nu --testbin repeater (0x[010203]) 5 | bytes ends-with 0x[010204] + "#); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn short_stream_binary_overflow() { + let actual = nu!(r#" + nu --testbin repeater (0x[01]) 5 | bytes ends-with 0x[010101010101] + "#); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn long_stream_binary() { + let actual = nu!(r#" + nu --testbin repeater (0x[01]) 32768 | bytes ends-with 0x[010101] + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn long_stream_binary_overflow() { + // .. ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + nu --testbin repeater (0x[01]) 32768 | bytes ends-with (0..32768 | each {|| 0x[01] } | bytes collect) + "#); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn long_stream_binary_exact() { + // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + nu --testbin repeater (0x[01020304]) 8192 | bytes ends-with (0..<8192 | each {|| 0x[01020304] } | bytes collect) + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn long_stream_string_exact() { + // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + nu --testbin repeater hell 8192 | bytes ends-with (0..<8192 | each {|| "hell" | into binary } | bytes collect) + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn long_stream_mixed_exact() { + // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect) + let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect) + + nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes ends-with (bytes build $binseg $strseg) + "#); + + assert_eq!( + actual.err, "", + "invocation failed. command line limit likely reached" + ); + assert_eq!(actual.out, "true"); +} + +#[test] +fn long_stream_mixed_overflow() { + // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect) + let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect) + + nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes ends-with (bytes build 0x[01] $binseg $strseg) + "#); + + assert_eq!( + actual.err, "", + "invocation failed. command line limit likely reached" + ); + assert_eq!(actual.out, "false"); +} diff --git a/nushell/crates/nu-cmd-extra/tests/commands/bytes/mod.rs b/nushell/crates/nu-cmd-extra/tests/commands/bytes/mod.rs new file mode 100644 index 0000000..a8a241e --- /dev/null +++ b/nushell/crates/nu-cmd-extra/tests/commands/bytes/mod.rs @@ -0,0 +1,2 @@ +mod ends_with; +mod starts_with; diff --git a/nushell/crates/nu-cmd-extra/tests/commands/bytes/starts_with.rs b/nushell/crates/nu-cmd-extra/tests/commands/bytes/starts_with.rs new file mode 100644 index 0000000..e7d5769 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/tests/commands/bytes/starts_with.rs @@ -0,0 +1,120 @@ +use nu_test_support::nu; + +#[test] +fn basic_binary_starts_with() { + let actual = nu!(r#" + "hello world" | into binary | bytes starts-with 0x[68 65 6c 6c 6f] + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn basic_string_fails() { + let actual = nu!(r#" + "hello world" | bytes starts-with 0x[68 65 6c 6c 6f] + "#); + + assert!(actual.err.contains("command doesn't support")); + assert_eq!(actual.out, ""); +} + +#[test] +fn short_stream_binary() { + let actual = nu!(r#" + nu --testbin repeater (0x[01]) 5 | bytes starts-with 0x[010101] + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn short_stream_mismatch() { + let actual = nu!(r#" + nu --testbin repeater (0x[010203]) 5 | bytes starts-with 0x[010204] + "#); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn short_stream_binary_overflow() { + let actual = nu!(r#" + nu --testbin repeater (0x[01]) 5 | bytes starts-with 0x[010101010101] + "#); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn long_stream_binary() { + let actual = nu!(r#" + nu --testbin repeater (0x[01]) 32768 | bytes starts-with 0x[010101] + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn long_stream_binary_overflow() { + // .. ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + nu --testbin repeater (0x[01]) 32768 | bytes starts-with (0..32768 | each {|| 0x[01] } | bytes collect) + "#); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn long_stream_binary_exact() { + // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + nu --testbin repeater (0x[01020304]) 8192 | bytes starts-with (0..<8192 | each {|| 0x[01020304] } | bytes collect) + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn long_stream_string_exact() { + // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + nu --testbin repeater hell 8192 | bytes starts-with (0..<8192 | each {|| "hell" | into binary } | bytes collect) + "#); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn long_stream_mixed_exact() { + // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect) + let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect) + + nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes starts-with (bytes build $binseg $strseg) + "#); + + assert_eq!( + actual.err, "", + "invocation failed. command line limit likely reached" + ); + assert_eq!(actual.out, "true"); +} + +#[test] +fn long_stream_mixed_overflow() { + // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow + let actual = nu!(r#" + let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect) + let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect) + + nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes starts-with (bytes build $binseg $strseg 0x[01]) + "#); + + assert_eq!( + actual.err, "", + "invocation failed. command line limit likely reached" + ); + assert_eq!(actual.out, "false"); +} diff --git a/nushell/crates/nu-cmd-extra/tests/commands/mod.rs b/nushell/crates/nu-cmd-extra/tests/commands/mod.rs new file mode 100644 index 0000000..fd216ce --- /dev/null +++ b/nushell/crates/nu-cmd-extra/tests/commands/mod.rs @@ -0,0 +1,2 @@ +mod bits; +mod bytes; diff --git a/nushell/crates/nu-cmd-extra/tests/main.rs b/nushell/crates/nu-cmd-extra/tests/main.rs new file mode 100644 index 0000000..f3d4468 --- /dev/null +++ b/nushell/crates/nu-cmd-extra/tests/main.rs @@ -0,0 +1 @@ +mod commands; diff --git a/nushell/crates/nu-cmd-lang/Cargo.toml b/nushell/crates/nu-cmd-lang/Cargo.toml new file mode 100644 index 0000000..0f371cc --- /dev/null +++ b/nushell/crates/nu-cmd-lang/Cargo.toml @@ -0,0 +1,45 @@ +[package] +authors = ["The Nushell Project Developers"] +build = "build.rs" +description = "Nushell's core language commands" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang" +edition = "2024" +license = "MIT" +name = "nu-cmd-lang" +version = "0.105.2" + +[lib] +bench = false + +[lints] +workspace = true + +[dependencies] +nu-engine = { path = "../nu-engine", version = "0.105.2", default-features = false } +nu-parser = { path = "../nu-parser", version = "0.105.2" } +nu-protocol = { path = "../nu-protocol", version = "0.105.2", default-features = false } +nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false } +nu-cmd-base = { path = "../nu-cmd-base", version = "0.105.2" } + +itertools = { workspace = true } +shadow-rs = { version = "1.2", default-features = false } + +[build-dependencies] +shadow-rs = { version = "1.2", default-features = false, features = ["build"] } + +[dev-dependencies] +quickcheck = { workspace = true } +quickcheck_macros = { workspace = true } +miette = { workspace = true } + +[features] +default = ["os"] +os = [ + "nu-engine/os", + "nu-protocol/os", + "nu-utils/os", +] +plugin = [ + "nu-protocol/plugin", + "os", +] diff --git a/nushell/crates/nu-cmd-lang/LICENSE b/nushell/crates/nu-cmd-lang/LICENSE new file mode 100644 index 0000000..ae174e8 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nushell/crates/nu-cmd-lang/README.md b/nushell/crates/nu-cmd-lang/README.md new file mode 100644 index 0000000..3ebfd93 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/README.md @@ -0,0 +1,21 @@ +# nu-cmd-lang + +## the base language and command crate of nu + +The commands in this crate are the *core commands* of the nu language. +It is also the base crate upon which all other command crates sit on +top of including: + +* nu-command +* nu-cli +* nu-cmd-extra + +As time goes on and the nu language develops further in parallel with nushell we will be adding other command crates to the system. + +### What does it mean to be a base crate ? + +A base crate is one with minimal dependencies in our system so that other developers can come along and use this crate without having a lot of baggage in terms of other crates which will bloat their underlying application. + +### Background on nu-cmd-lang + +This crate was designed to be a small, concise set of tools or commands that serve as the *foundation layer* of both nu and nushell. These are the core commands needed to have a nice working version of the *nu language* without all of the support that the other commands provide inside nushell. Prior to the launch of this crate all of our commands were housed in the crate *nu-command*. Moving forward we would like to *slowly* break out the commands in nu-command into different crates; the naming and how this will work and where all the commands will be located is a "work in progress" especially now that the *standard library* is starting to become more popular as a location for commands. As time goes on some of our commands written in rust will be migrated to nu and when this happens they will be moved into the *standard library*. diff --git a/nushell/crates/nu-cmd-lang/build.rs b/nushell/crates/nu-cmd-lang/build.rs new file mode 100644 index 0000000..8f2339b --- /dev/null +++ b/nushell/crates/nu-cmd-lang/build.rs @@ -0,0 +1,21 @@ +use std::process::Command; + +fn main() { + // Look up the current Git commit ourselves instead of relying on shadow_rs, + // because shadow_rs does it in a really slow-to-compile way (it builds libgit2) + let hash = get_git_hash().unwrap_or_default(); + println!("cargo:rustc-env=NU_COMMIT_HASH={hash}"); + shadow_rs::ShadowBuilder::builder() + .build() + .expect("shadow builder build should success"); +} + +fn get_git_hash() -> Option { + Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .ok() + .filter(|output| output.status.success()) + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map(|hash| hash.trim().to_string()) +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/alias.rs b/nushell/crates/nu-cmd-lang/src/core_commands/alias.rs new file mode 100644 index 0000000..07f41bb --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/alias.rs @@ -0,0 +1,58 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Alias; + +impl Command for Alias { + fn name(&self) -> &str { + "alias" + } + + fn description(&self) -> &str { + "Alias a command (with optional flags) to a new name." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("alias") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("name", SyntaxShape::String, "Name of the alias.") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)), + "Equals sign followed by value.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn search_terms(&self) -> Vec<&str> { + vec!["abbr", "aka", "fn", "func", "function"] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Alias ll to ls -l", + example: "alias ll = ls -l", + result: Some(Value::nothing(Span::test_data())), + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/attr/category.rs b/nushell/crates/nu-cmd-lang/src/core_commands/attr/category.rs new file mode 100644 index 0000000..1633f43 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/attr/category.rs @@ -0,0 +1,61 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct AttrCategory; + +impl Command for AttrCategory { + fn name(&self) -> &str { + "attr category" + } + + fn signature(&self) -> Signature { + Signature::build("attr category") + .input_output_type(Type::Nothing, Type::list(Type::String)) + .allow_variants_without_examples(true) + .required( + "category", + SyntaxShape::String, + "Category of the custom command.", + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Attribute for adding a category to custom commands." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let arg: String = call.req(engine_state, stack, 0)?; + Ok(Value::string(arg, call.head).into_pipeline_data()) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let arg: String = call.req_const(working_set, 0)?; + Ok(Value::string(arg, call.head).into_pipeline_data()) + } + + fn is_const(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Add a category to a custom command", + example: r###"# Double numbers + @category math + def double []: [number -> number] { $in * 2 }"###, + result: None, + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/attr/deprecated.rs b/nushell/crates/nu-cmd-lang/src/core_commands/attr/deprecated.rs new file mode 100644 index 0000000..67a1298 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/attr/deprecated.rs @@ -0,0 +1,148 @@ +use nu_cmd_base::WrapCall; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct AttrDeprecated; + +impl Command for AttrDeprecated { + fn name(&self) -> &str { + "attr deprecated" + } + + fn signature(&self) -> Signature { + Signature::build("attr deprecated") + .input_output_types(vec![ + (Type::Nothing, Type::Nothing), + (Type::Nothing, Type::String), + ]) + .optional( + "message", + SyntaxShape::String, + "Help message to include with deprecation warning.", + ) + .named( + "flag", + SyntaxShape::String, + "Mark a flag as deprecated rather than the command", + None, + ) + .named( + "since", + SyntaxShape::String, + "Denote a version when this item was deprecated", + Some('s'), + ) + .named( + "remove", + SyntaxShape::String, + "Denote a version when this item will be removed", + Some('r'), + ) + .named( + "report", + SyntaxShape::String, + "How to warn about this item. One of: first (default), every", + None, + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Attribute for marking a command or flag as deprecated." + } + + fn extra_description(&self) -> &str { + "Mark a command (default) or flag/switch (--flag) as deprecated. By default, only the first usage will trigger a deprecation warning. + +A help message can be included to provide more context for the deprecation, such as what to use as a replacement. + +Also consider setting the category to deprecated with @category deprecated" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let call = WrapCall::Eval(engine_state, stack, call); + Ok(deprecated_record(call)?.into_pipeline_data()) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let call = WrapCall::ConstEval(working_set, call); + Ok(deprecated_record(call)?.into_pipeline_data()) + } + + fn is_const(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Add a deprecation warning to a custom command", + example: r###"@deprecated + def outdated [] {}"###, + result: Some(Value::nothing(Span::test_data())), + }, + Example { + description: "Add a deprecation warning with a custom message", + example: r###"@deprecated "Use my-new-command instead." + @category deprecated + def my-old-command [] {}"###, + result: Some(Value::string( + "Use my-new-command instead.", + Span::test_data(), + )), + }, + ] + } +} + +fn deprecated_record(call: WrapCall) -> Result { + let (call, message): (_, Option>) = call.opt(0)?; + let (call, flag): (_, Option>) = call.get_flag("flag")?; + let (call, since): (_, Option>) = call.get_flag("since")?; + let (call, remove): (_, Option>) = call.get_flag("remove")?; + let (call, report): (_, Option>) = call.get_flag("report")?; + + let mut record = Record::new(); + if let Some(message) = message { + record.push("help", Value::string(message.item, message.span)) + } + if let Some(flag) = flag { + record.push("flag", Value::string(flag.item, flag.span)) + } + if let Some(since) = since { + record.push("since", Value::string(since.item, since.span)) + } + if let Some(remove) = remove { + record.push("expected_removal", Value::string(remove.item, remove.span)) + } + + let report = if let Some(Spanned { item, span }) = report { + match item.as_str() { + "every" => Value::string(item, span), + "first" => Value::string(item, span), + _ => { + return Err(ShellError::IncorrectValue { + msg: "The report mode must be one of: every, first".into(), + val_span: span, + call_span: call.head(), + }); + } + } + } else { + Value::string("first", call.head()) + }; + record.push("report", report); + + Ok(Value::record(record, call.head())) +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/attr/example.rs b/nushell/crates/nu-cmd-lang/src/core_commands/attr/example.rs new file mode 100644 index 0000000..a12f83d --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/attr/example.rs @@ -0,0 +1,159 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct AttrExample; + +impl Command for AttrExample { + fn name(&self) -> &str { + "attr example" + } + + // TODO: When const closure are available, switch to using them for the `example` argument + // rather than a block. That should remove the need for `requires_ast_for_arguments` to be true + fn signature(&self) -> Signature { + Signature::build("attr example") + .input_output_types(vec![( + Type::Nothing, + Type::Record( + [ + ("description".into(), Type::String), + ("example".into(), Type::String), + ] + .into(), + ), + )]) + .allow_variants_without_examples(true) + .required( + "description", + SyntaxShape::String, + "Description of the example.", + ) + .required( + "example", + SyntaxShape::OneOf(vec![SyntaxShape::Block, SyntaxShape::String]), + "Example code snippet.", + ) + .named( + "result", + SyntaxShape::Any, + "Expected output of example.", + None, + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Attribute for adding examples to custom commands." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let description: Spanned = call.req(engine_state, stack, 0)?; + let result: Option = call.get_flag(engine_state, stack, "result")?; + + let example_string: Result = call.req(engine_state, stack, 1); + let example_expr = call + .positional_nth(stack, 1) + .ok_or(ShellError::MissingParameter { + param_name: "example".into(), + span: call.head, + })?; + + let working_set = StateWorkingSet::new(engine_state); + + attr_example_impl( + example_expr, + example_string, + &working_set, + call, + description, + result, + ) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let description: Spanned = call.req_const(working_set, 0)?; + let result: Option = call.get_flag_const(working_set, "result")?; + + let example_string: Result = call.req_const(working_set, 1); + let example_expr = + call.assert_ast_call()? + .positional_nth(1) + .ok_or(ShellError::MissingParameter { + param_name: "example".into(), + span: call.head, + })?; + + attr_example_impl( + example_expr, + example_string, + working_set, + call, + description, + result, + ) + } + + fn is_const(&self) -> bool { + true + } + + fn requires_ast_for_arguments(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Add examples to custom command", + example: r###"# Double numbers + @example "double an int" { 2 | double } --result 4 + @example "double a float" { 0.25 | double } --result 0.5 + def double []: [number -> number] { $in * 2 }"###, + result: None, + }] + } +} + +fn attr_example_impl( + example_expr: &nu_protocol::ast::Expression, + example_string: Result, + working_set: &StateWorkingSet<'_>, + call: &Call<'_>, + description: Spanned, + result: Option, +) -> Result { + let example_content = match example_expr.as_block() { + Some(block_id) => { + let block = working_set.get_block(block_id); + let contents = + working_set.get_span_contents(block.span.expect("a block must have a span")); + let contents = contents + .strip_prefix(b"{") + .and_then(|x| x.strip_suffix(b"}")) + .unwrap_or(contents) + .trim_ascii(); + String::from_utf8_lossy(contents).into_owned() + } + None => example_string?, + }; + + let mut rec = record! { + "description" => Value::string(description.item, description.span), + "example" => Value::string(example_content, example_expr.span), + }; + if let Some(result) = result { + rec.push("result", result); + } + + Ok(Value::record(rec, call.head).into_pipeline_data()) +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/attr/mod.rs b/nushell/crates/nu-cmd-lang/src/core_commands/attr/mod.rs new file mode 100644 index 0000000..8791bb4 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/attr/mod.rs @@ -0,0 +1,9 @@ +mod category; +mod deprecated; +mod example; +mod search_terms; + +pub use category::AttrCategory; +pub use deprecated::AttrDeprecated; +pub use example::AttrExample; +pub use search_terms::AttrSearchTerms; diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/attr/search_terms.rs b/nushell/crates/nu-cmd-lang/src/core_commands/attr/search_terms.rs new file mode 100644 index 0000000..767bbea --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/attr/search_terms.rs @@ -0,0 +1,57 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct AttrSearchTerms; + +impl Command for AttrSearchTerms { + fn name(&self) -> &str { + "attr search-terms" + } + + fn signature(&self) -> Signature { + Signature::build("attr search-terms") + .input_output_type(Type::Nothing, Type::list(Type::String)) + .allow_variants_without_examples(true) + .rest("terms", SyntaxShape::String, "Search terms.") + .category(Category::Core) + } + + fn description(&self) -> &str { + "Attribute for adding search terms to custom commands." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let args = call.rest(engine_state, stack, 0)?; + Ok(Value::list(args, call.head).into_pipeline_data()) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let args = call.rest_const(working_set, 0)?; + Ok(Value::list(args, call.head).into_pipeline_data()) + } + + fn is_const(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Add search terms to a custom command", + example: r###"# Double numbers + @search-terms multiply times + def double []: [number -> number] { $in * 2 }"###, + result: None, + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/break_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/break_.rs new file mode 100644 index 0000000..7fdf5a7 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/break_.rs @@ -0,0 +1,55 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Break; + +impl Command for Break { + fn name(&self) -> &str { + "break" + } + + fn description(&self) -> &str { + "Break a loop." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("break") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html + + break can only be used in while, loop, and for loops. It can not be used with each or other filter commands"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'break' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Break out of a loop", + example: r#"loop { break }"#, + result: None, + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/collect.rs b/nushell/crates/nu-cmd-lang/src/core_commands/collect.rs new file mode 100644 index 0000000..654f165 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/collect.rs @@ -0,0 +1,129 @@ +use nu_engine::{command_prelude::*, get_eval_block, redirect_env}; +use nu_protocol::{DataSource, PipelineMetadata, engine::Closure}; + +#[derive(Clone)] +pub struct Collect; + +impl Command for Collect { + fn name(&self) -> &str { + "collect" + } + + fn signature(&self) -> Signature { + Signature::build("collect") + .input_output_types(vec![(Type::Any, Type::Any)]) + .optional( + "closure", + SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), + "The closure to run once the stream is collected.", + ) + .switch( + "keep-env", + "let the closure affect environment variables", + None, + ) + .category(Category::Filters) + } + + fn description(&self) -> &str { + "Collect a stream into a value." + } + + fn extra_description(&self) -> &str { + r#"If provided, run a closure with the collected value as input. + +The entire stream will be collected into one value in memory, so if the stream +is particularly large, this can cause high memory usage."# + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let closure: Option = call.opt(engine_state, stack, 0)?; + + let metadata = match input.metadata() { + // Remove the `FilePath` metadata, because after `collect` it's no longer necessary to + // check where some input came from. + Some(PipelineMetadata { + data_source: DataSource::FilePath(_), + content_type: None, + }) => None, + other => other, + }; + + let input = input.into_value(call.head)?; + let result; + + if let Some(closure) = closure { + let block = engine_state.get_block(closure.block_id); + let mut stack_captures = + stack.captures_to_stack_preserve_out_dest(closure.captures.clone()); + + let mut saved_positional = None; + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack_captures.add_var(*var_id, input.clone()); + saved_positional = Some(*var_id); + } + } + + let eval_block = get_eval_block(engine_state); + + result = eval_block( + engine_state, + &mut stack_captures, + block, + input.into_pipeline_data_with_metadata(metadata), + ); + + if call.has_flag(engine_state, stack, "keep-env")? { + redirect_env(engine_state, stack, &stack_captures); + // for when we support `data | let x = $in;` + // remove the variables added earlier + for (var_id, _) in closure.captures { + stack_captures.remove_var(var_id); + } + if let Some(u) = saved_positional { + stack_captures.remove_var(u); + } + // add any new variables to the stack + stack.vars.extend(stack_captures.vars); + } + } else { + result = Ok(input.into_pipeline_data_with_metadata(metadata)); + } + + result + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Use the second value in the stream", + example: "[1 2 3] | collect { |x| $x.1 }", + result: Some(Value::test_int(2)), + }, + Example { + description: "Read and write to the same file", + example: "open file.txt | collect | save -f file.txt", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Collect {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/const_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/const_.rs new file mode 100644 index 0000000..cfb5b66 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/const_.rs @@ -0,0 +1,103 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Const; + +impl Command for Const { + fn name(&self) -> &str { + "const" + } + + fn description(&self) -> &str { + "Create a parse-time constant." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("const") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("const_name", SyntaxShape::VarWithOptType, "Constant name.") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), + "Equals sign followed by constant value.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn search_terms(&self) -> Vec<&str> { + vec!["set", "let"] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'const' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn run_const( + &self, + _working_set: &StateWorkingSet, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn is_const(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Create a new parse-time constant.", + example: "const x = 10", + result: None, + }, + Example { + description: "Create a composite constant value", + example: "const x = { a: 10, b: 20 }", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use nu_protocol::engine::CommandType; + + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Const {}) + } + + #[test] + fn test_command_type() { + assert!(matches!(Const.command_type(), CommandType::Keyword)); + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/continue_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/continue_.rs new file mode 100644 index 0000000..bb9c3e4 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/continue_.rs @@ -0,0 +1,54 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Continue; + +impl Command for Continue { + fn name(&self) -> &str { + "continue" + } + + fn description(&self) -> &str { + "Continue a loop from the next iteration." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("continue") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html + + continue can only be used in while, loop, and for loops. It can not be used with each or other filter commands"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'continue' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Continue a loop from the next iteration", + example: r#"for i in 1..10 { if $i == 5 { continue }; print $i }"#, + result: None, + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/def.rs b/nushell/crates/nu-cmd-lang/src/core_commands/def.rs new file mode 100644 index 0000000..efb5bdf --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/def.rs @@ -0,0 +1,80 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Def; + +impl Command for Def { + fn name(&self) -> &str { + "def" + } + + fn description(&self) -> &str { + "Define a custom command." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("def") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("def_name", SyntaxShape::String, "Command name.") + .required("params", SyntaxShape::Signature, "Parameters.") + .required("block", SyntaxShape::Closure(None), "Body of the definition.") + .switch("env", "keep the environment defined inside the command", None) + .switch("wrapped", "treat unknown flags and arguments as strings (requires ...rest-like parameter in signature)", None) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Define a command and run it", + example: r#"def say-hi [] { echo 'hi' }; say-hi"#, + result: Some(Value::test_string("hi")), + }, + Example { + description: "Define a command and run it with parameter(s)", + example: r#"def say-sth [sth: string] { echo $sth }; say-sth hi"#, + result: Some(Value::test_string("hi")), + }, + Example { + description: "Set environment variable by call a custom command", + example: r#"def --env foo [] { $env.BAR = "BAZ" }; foo; $env.BAR"#, + result: Some(Value::test_string("BAZ")), + }, + Example { + description: "cd affects the environment, so '--env' is required to change directory from within a command", + example: r#"def --env gohome [] { cd ~ }; gohome; $env.PWD == ('~' | path expand)"#, + result: Some(Value::test_string("true")), + }, + Example { + description: "Define a custom wrapper for an external command", + example: r#"def --wrapped my-echo [...rest] { ^echo ...$rest }; my-echo -e 'spam\tspam'"#, + result: Some(Value::test_string("spam\tspam")), + }, + Example { + description: "Define a custom command with a type signature. Passing a non-int value will result in an error", + example: r#"def only_int []: int -> int { $in }; 42 | only_int"#, + result: Some(Value::test_int(42)), + }, + ] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/describe.rs b/nushell/crates/nu-cmd-lang/src/core_commands/describe.rs new file mode 100644 index 0000000..d473b2b --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/describe.rs @@ -0,0 +1,506 @@ +use nu_engine::command_prelude::*; +use nu_protocol::{ + BlockId, ByteStreamSource, Category, PipelineMetadata, Signature, + engine::{Closure, StateWorkingSet}, +}; +use std::any::type_name; +#[derive(Clone)] +pub struct Describe; + +impl Command for Describe { + fn name(&self) -> &str { + "describe" + } + + fn description(&self) -> &str { + "Describe the type and structure of the value(s) piped in." + } + + fn signature(&self) -> Signature { + Signature::build("describe") + .input_output_types(vec![(Type::Any, Type::Any)]) + .switch( + "no-collect", + "do not collect streams of structured data", + Some('n'), + ) + .switch( + "detailed", + "show detailed information about the value", + Some('d'), + ) + .category(Category::Core) + } + + fn is_const(&self) -> bool { + true + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let options = Options { + no_collect: call.has_flag(engine_state, stack, "no-collect")?, + detailed: call.has_flag(engine_state, stack, "detailed")?, + }; + run(Some(engine_state), call, input, options) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let options = Options { + no_collect: call.has_flag_const(working_set, "no-collect")?, + detailed: call.has_flag_const(working_set, "detailed")?, + }; + run(None, call, input, options) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Describe the type of a string", + example: "'hello' | describe", + result: Some(Value::test_string("string")), + }, + Example { + description: "Describe the type of a record in a detailed way", + example: "{shell:'true', uwu:true, features: {bugs:false, multiplatform:true, speed: 10}, fib: [1 1 2 3 5 8], on_save: {|x| $'Saving ($x)'}, first_commit: 2019-05-10, my_duration: (4min + 20sec)} | describe -d", + result: Some(Value::test_record(record!( + "type" => Value::test_string("record"), + "detailed_type" => Value::test_string("record, fib: list, on_save: closure, first_commit: datetime, my_duration: duration>"), + "columns" => Value::test_record(record!( + "shell" => Value::test_record(record!( + "type" => Value::test_string("string"), + "detailed_type" => Value::test_string("string"), + "rust_type" => Value::test_string("&alloc::string::String"), + "value" => Value::test_string("true"), + )), + "uwu" => Value::test_record(record!( + "type" => Value::test_string("bool"), + "detailed_type" => Value::test_string("bool"), + "rust_type" => Value::test_string("bool"), + "value" => Value::test_bool(true), + )), + "features" => Value::test_record(record!( + "type" => Value::test_string("record"), + "detailed_type" => Value::test_string("record"), + "columns" => Value::test_record(record!( + "bugs" => Value::test_record(record!( + "type" => Value::test_string("bool"), + "detailed_type" => Value::test_string("bool"), + "rust_type" => Value::test_string("bool"), + "value" => Value::test_bool(false), + )), + "multiplatform" => Value::test_record(record!( + "type" => Value::test_string("bool"), + "detailed_type" => Value::test_string("bool"), + "rust_type" => Value::test_string("bool"), + "value" => Value::test_bool(true), + )), + "speed" => Value::test_record(record!( + "type" => Value::test_string("int"), + "detailed_type" => Value::test_string("int"), + "rust_type" => Value::test_string("i64"), + "value" => Value::test_int(10), + )), + )), + "rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow"), + )), + "fib" => Value::test_record(record!( + "type" => Value::test_string("list"), + "detailed_type" => Value::test_string("list"), + "length" => Value::test_int(6), + "rust_type" => Value::test_string("&mut alloc::vec::Vec"), + "value" => Value::test_list(vec![ + Value::test_record(record!( + "type" => Value::test_string("int"), + "detailed_type" => Value::test_string("int"), + "rust_type" => Value::test_string("i64"), + "value" => Value::test_int(1), + )), + Value::test_record(record!( + "type" => Value::test_string("int"), + "detailed_type" => Value::test_string("int"), + "rust_type" => Value::test_string("i64"), + "value" => Value::test_int(1), + )), + Value::test_record(record!( + "type" => Value::test_string("int"), + "detailed_type" => Value::test_string("int"), + "rust_type" => Value::test_string("i64"), + "value" => Value::test_int(2), + )), + Value::test_record(record!( + "type" => Value::test_string("int"), + "detailed_type" => Value::test_string("int"), + "rust_type" => Value::test_string("i64"), + "value" => Value::test_int(3), + )), + Value::test_record(record!( + "type" => Value::test_string("int"), + "detailed_type" => Value::test_string("int"), + "rust_type" => Value::test_string("i64"), + "value" => Value::test_int(5), + )), + Value::test_record(record!( + "type" => Value::test_string("int"), + "detailed_type" => Value::test_string("int"), + "rust_type" => Value::test_string("i64"), + "value" => Value::test_int(8), + ))] + ), + )), + "on_save" => Value::test_record(record!( + "type" => Value::test_string("closure"), + "detailed_type" => Value::test_string("closure"), + "rust_type" => Value::test_string("&alloc::boxed::Box"), + "value" => Value::test_closure(Closure { + block_id: BlockId::new(1), + captures: vec![], + }), + "signature" => Value::test_record(record!( + "name" => Value::test_string(""), + "category" => Value::test_string("default"), + )), + )), + "first_commit" => Value::test_record(record!( + "type" => Value::test_string("datetime"), + "detailed_type" => Value::test_string("datetime"), + "rust_type" => Value::test_string("chrono::datetime::DateTime"), + "value" => Value::test_date("2019-05-10 00:00:00Z".parse().unwrap_or_default()), + )), + "my_duration" => Value::test_record(record!( + "type" => Value::test_string("duration"), + "detailed_type" => Value::test_string("duration"), + "rust_type" => Value::test_string("i64"), + "value" => Value::test_duration(260_000_000_000), + )) + )), + "rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow"), + ))), + }, + Example { + description: "Describe the type of a stream with detailed information", + example: "[1 2 3] | each {|i| echo $i} | describe -d", + result: None, // Give "Running external commands not supported" error + // result: Some(Value::test_record(record!( + // "type" => Value::test_string("stream"), + // "origin" => Value::test_string("nushell"), + // "subtype" => Value::test_record(record!( + // "type" => Value::test_string("list"), + // "length" => Value::test_int(3), + // "values" => Value::test_list(vec![ + // Value::test_string("int"), + // Value::test_string("int"), + // Value::test_string("int"), + // ]) + // )) + // ))), + }, + Example { + description: "Describe a stream of data, collecting it first", + example: "[1 2 3] | each {|i| echo $i} | describe", + result: None, // Give "Running external commands not supported" error + // result: Some(Value::test_string("list (stream)")), + }, + Example { + description: "Describe the input but do not collect streams", + example: "[1 2 3] | each {|i| echo $i} | describe --no-collect", + result: None, // Give "Running external commands not supported" error + // result: Some(Value::test_string("stream")), + }, + ] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["type", "typeof", "info", "structure"] + } +} + +#[derive(Clone, Copy)] +struct Options { + no_collect: bool, + detailed: bool, +} + +fn run( + engine_state: Option<&EngineState>, + call: &Call, + input: PipelineData, + options: Options, +) -> Result { + let head = call.head; + let metadata = input.metadata(); + + let description = match input { + PipelineData::ByteStream(stream, ..) => { + let type_ = stream.type_().describe(); + + let description = if options.detailed { + let origin = match stream.source() { + ByteStreamSource::Read(_) => "unknown", + ByteStreamSource::File(_) => "file", + #[cfg(feature = "os")] + ByteStreamSource::Child(_) => "external", + }; + + Value::record( + record! { + "type" => Value::string("bytestream", head), + "detailed_type" => Value::string(type_, head), + "rust_type" => Value::string(type_of(&stream), head), + "origin" => Value::string(origin, head), + "metadata" => metadata_to_value(metadata, head), + }, + head, + ) + } else { + Value::string(type_, head) + }; + + if !options.no_collect { + stream.drain()?; + } + + description + } + PipelineData::ListStream(stream, ..) => { + let type_ = type_of(&stream); + if options.detailed { + let subtype = if options.no_collect { + Value::string("any", head) + } else { + describe_value(stream.into_value(), head, engine_state) + }; + Value::record( + record! { + "type" => Value::string("stream", head), + "detailed_type" => Value::string("list stream", head), + "rust_type" => Value::string(type_, head), + "origin" => Value::string("nushell", head), + "subtype" => subtype, + "metadata" => metadata_to_value(metadata, head), + }, + head, + ) + } else if options.no_collect { + Value::string("stream", head) + } else { + let value = stream.into_value(); + let base_description = value.get_type().to_string(); + Value::string(format!("{} (stream)", base_description), head) + } + } + PipelineData::Value(value, ..) => { + if !options.detailed { + Value::string(value.get_type().to_string(), head) + } else { + describe_value(value, head, engine_state) + } + } + PipelineData::Empty => Value::string(Type::Nothing.to_string(), head), + }; + + Ok(description.into_pipeline_data()) +} + +enum Description { + Record(Record), +} + +impl Description { + fn into_value(self, span: Span) -> Value { + match self { + Description::Record(record) => Value::record(record, span), + } + } +} + +fn describe_value(value: Value, head: Span, engine_state: Option<&EngineState>) -> Value { + let Description::Record(record) = describe_value_inner(value, head, engine_state); + Value::record(record, head) +} + +fn type_of(_: &T) -> String { + type_name::().to_string() +} + +fn describe_value_inner( + mut value: Value, + head: Span, + engine_state: Option<&EngineState>, +) -> Description { + let value_type = value.get_type().to_string(); + match value { + Value::Bool { val, .. } => Description::Record(record! { + "type" => Value::string("bool", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::Int { val, .. } => Description::Record(record! { + "type" => Value::string("int", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::Float { val, .. } => Description::Record(record! { + "type" => Value::string("float", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::Filesize { val, .. } => Description::Record(record! { + "type" => Value::string("filesize", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::Duration { val, .. } => Description::Record(record! { + "type" => Value::string("duration", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::Date { val, .. } => Description::Record(record! { + "type" => Value::string("datetime", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::Range { ref val, .. } => Description::Record(record! { + "type" => Value::string("range", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::String { ref val, .. } => Description::Record(record! { + "type" => Value::string("string", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::Glob { ref val, .. } => Description::Record(record! { + "type" => Value::string("glob", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::Nothing { .. } => Description::Record(record! { + "type" => Value::string("nothing", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string("", head), + "value" => value, + }), + Value::Record { ref val, .. } => { + let mut columns = val.clone().into_owned(); + for (_, val) in &mut columns { + *val = + describe_value_inner(std::mem::take(val), head, engine_state).into_value(head); + } + + Description::Record(record! { + "type" => Value::string("record", head), + "detailed_type" => Value::string(value_type, head), + "columns" => Value::record(columns.clone(), head), + "rust_type" => Value::string(type_of(&val), head), + }) + } + Value::List { ref mut vals, .. } => { + for val in &mut *vals { + *val = + describe_value_inner(std::mem::take(val), head, engine_state).into_value(head); + } + + Description::Record(record! { + "type" => Value::string("list", head), + "detailed_type" => Value::string(value_type, head), + "length" => Value::int(vals.len() as i64, head), + "rust_type" => Value::string(type_of(&vals), head), + "value" => value, + }) + } + Value::Closure { ref val, .. } => { + let block = engine_state.map(|engine_state| engine_state.get_block(val.block_id)); + + let mut record = record! { + "type" => Value::string("closure", head), + "detailed_type" => Value::string(value_type, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }; + if let Some(block) = block { + record.push( + "signature", + Value::record( + record! { + "name" => Value::string(block.signature.name.clone(), head), + "category" => Value::string(block.signature.category.to_string(), head), + }, + head, + ), + ); + } + Description::Record(record) + } + Value::Error { ref error, .. } => Description::Record(record! { + "type" => Value::string("error", head), + "detailed_type" => Value::string(value_type, head), + "subtype" => Value::string(error.to_string(), head), + "rust_type" => Value::string(type_of(&error), head), + "value" => value, + }), + Value::Binary { ref val, .. } => Description::Record(record! { + "type" => Value::string("binary", head), + "detailed_type" => Value::string(value_type, head), + "length" => Value::int(val.len() as i64, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value, + }), + Value::CellPath { ref val, .. } => Description::Record(record! { + "type" => Value::string("cell-path", head), + "detailed_type" => Value::string(value_type, head), + "length" => Value::int(val.members.len() as i64, head), + "rust_type" => Value::string(type_of(&val), head), + "value" => value + }), + Value::Custom { ref val, .. } => Description::Record(record! { + "type" => Value::string("custom", head), + "detailed_type" => Value::string(value_type, head), + "subtype" => Value::string(val.type_name(), head), + "rust_type" => Value::string(type_of(&val), head), + "value" => + match val.to_base_value(head) { + Ok(base_value) => base_value, + Err(err) => Value::error(err, head), + } + }), + } +} + +fn metadata_to_value(metadata: Option, head: Span) -> Value { + if let Some(metadata) = metadata { + let data_source = Value::string(format!("{:?}", metadata.data_source), head); + Value::record(record! { "data_source" => data_source }, head) + } else { + Value::nothing(head) + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Describe; + use crate::test_examples; + test_examples(Describe {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/do_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/do_.rs new file mode 100644 index 0000000..6c94f9e --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/do_.rs @@ -0,0 +1,318 @@ +use nu_engine::{command_prelude::*, get_eval_block_with_early_return, redirect_env}; +#[cfg(feature = "os")] +use nu_protocol::process::{ChildPipe, ChildProcess}; +use nu_protocol::{ + ByteStream, ByteStreamSource, OutDest, engine::Closure, shell_error::io::IoError, +}; + +use std::{ + io::{Cursor, Read}, + thread, +}; + +#[derive(Clone)] +pub struct Do; + +impl Command for Do { + fn name(&self) -> &str { + "do" + } + + fn description(&self) -> &str { + "Run a closure, providing it with the pipeline input." + } + + fn signature(&self) -> Signature { + Signature::build("do") + .required("closure", SyntaxShape::Closure(None), "The closure to run.") + .input_output_types(vec![(Type::Any, Type::Any)]) + .switch( + "ignore-errors", + "ignore errors as the closure runs", + Some('i'), + ) + .switch( + "capture-errors", + "catch errors as the closure runs, and return them", + Some('c'), + ) + .switch( + "env", + "keep the environment defined inside the command", + None, + ) + .rest( + "rest", + SyntaxShape::Any, + "The parameter(s) for the closure.", + ) + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + caller_stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let block: Closure = call.req(engine_state, caller_stack, 0)?; + let rest: Vec = call.rest(engine_state, caller_stack, 1)?; + let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?; + + let capture_errors = call.has_flag(engine_state, caller_stack, "capture-errors")?; + let has_env = call.has_flag(engine_state, caller_stack, "env")?; + + let mut callee_stack = caller_stack.captures_to_stack_preserve_out_dest(block.captures); + let block = engine_state.get_block(block.block_id); + + bind_args_to(&mut callee_stack, &block.signature, rest, head)?; + let eval_block_with_early_return = get_eval_block_with_early_return(engine_state); + + let result = eval_block_with_early_return(engine_state, &mut callee_stack, block, input); + + if has_env { + // Merge the block's environment to the current stack + redirect_env(engine_state, caller_stack, &callee_stack); + } + + match result { + Ok(PipelineData::ByteStream(stream, metadata)) if capture_errors => { + let span = stream.span(); + #[cfg(not(feature = "os"))] + return Err(ShellError::DisabledOsSupport { + msg: "Cannot create a thread to receive stdout message.".to_string(), + span: Some(span), + }); + + #[cfg(feature = "os")] + match stream.into_child() { + Ok(mut child) => { + // Use a thread to receive stdout message. + // Or we may get a deadlock if child process sends out too much bytes to stderr. + // + // For example: in normal linux system, stderr pipe's limit is 65535 bytes. + // if child process sends out 65536 bytes, the process will be hanged because no consumer + // consumes the first 65535 bytes + // So we need a thread to receive stdout message, then the current thread can continue to consume + // stderr messages. + let stdout_handler = child + .stdout + .take() + .map(|mut stdout| { + thread::Builder::new() + .name("stdout consumer".to_string()) + .spawn(move || { + let mut buf = Vec::new(); + stdout.read_to_end(&mut buf).map_err(|err| { + IoError::new_internal( + err, + "Could not read stdout to end", + nu_protocol::location!(), + ) + })?; + Ok::<_, ShellError>(buf) + }) + .map_err(|err| IoError::new(err, head, None)) + }) + .transpose()?; + + // Intercept stderr so we can return it in the error if the exit code is non-zero. + // The threading issues mentioned above dictate why we also need to intercept stdout. + let stderr_msg = match child.stderr.take() { + None => String::new(), + Some(mut stderr) => { + let mut buf = String::new(); + stderr + .read_to_string(&mut buf) + .map_err(|err| IoError::new(err, span, None))?; + buf + } + }; + + let stdout = if let Some(handle) = stdout_handler { + match handle.join() { + Err(err) => { + return Err(ShellError::ExternalCommand { + label: "Fail to receive external commands stdout message" + .to_string(), + help: format!("{err:?}"), + span, + }); + } + Ok(res) => Some(res?), + } + } else { + None + }; + + child.ignore_error(false); + child.wait()?; + + let mut child = ChildProcess::from_raw(None, None, None, span); + if let Some(stdout) = stdout { + child.stdout = Some(ChildPipe::Tee(Box::new(Cursor::new(stdout)))); + } + if !stderr_msg.is_empty() { + child.stderr = Some(ChildPipe::Tee(Box::new(Cursor::new(stderr_msg)))); + } + Ok(PipelineData::ByteStream( + ByteStream::child(child, span), + metadata, + )) + } + Err(stream) => Ok(PipelineData::ByteStream(stream, metadata)), + } + } + Ok(PipelineData::ByteStream(mut stream, metadata)) + if ignore_all_errors + && !matches!( + caller_stack.stdout(), + OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value + ) => + { + #[cfg(feature = "os")] + if let ByteStreamSource::Child(child) = stream.source_mut() { + child.ignore_error(true); + } + Ok(PipelineData::ByteStream(stream, metadata)) + } + Ok(PipelineData::Value(Value::Error { .. }, ..)) | Err(_) if ignore_all_errors => { + Ok(PipelineData::empty()) + } + Ok(PipelineData::ListStream(stream, metadata)) if ignore_all_errors => { + let stream = stream.map(move |value| { + if let Value::Error { .. } = value { + Value::nothing(head) + } else { + value + } + }); + Ok(PipelineData::ListStream(stream, metadata)) + } + r => r, + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Run the closure", + example: r#"do { echo hello }"#, + result: Some(Value::test_string("hello")), + }, + Example { + description: "Run a stored first-class closure", + example: r#"let text = "I am enclosed"; let hello = {|| echo $text}; do $hello"#, + result: Some(Value::test_string("I am enclosed")), + }, + Example { + description: "Run the closure and ignore both shell and external program errors", + example: r#"do --ignore-errors { thisisnotarealcommand }"#, + result: None, + }, + Example { + description: "Abort the pipeline if a program returns a non-zero exit code", + example: r#"do --capture-errors { nu --commands 'exit 1' } | myscarycommand"#, + result: None, + }, + Example { + description: "Run the closure with a positional, type-checked parameter", + example: r#"do {|x:int| 100 + $x } 77"#, + result: Some(Value::test_int(177)), + }, + Example { + description: "Run the closure with pipeline input", + example: r#"77 | do { 100 + $in }"#, + result: Some(Value::test_int(177)), + }, + Example { + description: "Run the closure with a default parameter value", + example: r#"77 | do {|x=100| $x + $in }"#, + result: Some(Value::test_int(177)), + }, + Example { + description: "Run the closure with two positional parameters", + example: r#"do {|x,y| $x + $y } 77 100"#, + result: Some(Value::test_int(177)), + }, + Example { + description: "Run the closure and keep changes to the environment", + example: r#"do --env { $env.foo = 'bar' }; $env.foo"#, + result: Some(Value::test_string("bar")), + }, + ] + } +} + +fn bind_args_to( + stack: &mut Stack, + signature: &Signature, + args: Vec, + head_span: Span, +) -> Result<(), ShellError> { + let mut val_iter = args.into_iter(); + for (param, required) in signature + .required_positional + .iter() + .map(|p| (p, true)) + .chain(signature.optional_positional.iter().map(|p| (p, false))) + { + let var_id = param + .var_id + .expect("internal error: all custom parameters must have var_ids"); + if let Some(result) = val_iter.next() { + let param_type = param.shape.to_type(); + if required && !result.is_subtype_of(¶m_type) { + return Err(ShellError::CantConvert { + to_type: param.shape.to_type().to_string(), + from_type: result.get_type().to_string(), + span: result.span(), + help: None, + }); + } + stack.add_var(var_id, result); + } else if let Some(value) = ¶m.default_value { + stack.add_var(var_id, value.to_owned()) + } else if !required { + stack.add_var(var_id, Value::nothing(head_span)) + } else { + return Err(ShellError::MissingParameter { + param_name: param.name.to_string(), + span: head_span, + }); + } + } + + if let Some(rest_positional) = &signature.rest_positional { + let mut rest_items = vec![]; + + for result in val_iter { + rest_items.push(result); + } + + let span = if let Some(rest_item) = rest_items.first() { + rest_item.span() + } else { + head_span + }; + + stack.add_var( + rest_positional + .var_id + .expect("Internal error: rest positional parameter lacks var_id"), + Value::list(rest_items, span), + ) + } + Ok(()) +} + +mod test { + #[test] + fn test_examples() { + use super::Do; + use crate::test_examples; + test_examples(Do {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/echo.rs b/nushell/crates/nu-cmd-lang/src/core_commands/echo.rs new file mode 100644 index 0000000..e260c64 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/echo.rs @@ -0,0 +1,91 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct Echo; + +impl Command for Echo { + fn name(&self) -> &str { + "echo" + } + + fn description(&self) -> &str { + "Returns its arguments, ignoring the piped-in value." + } + + fn signature(&self) -> Signature { + Signature::build("echo") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .rest("rest", SyntaxShape::Any, "The values to echo.") + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"Unlike `print`, which prints unstructured text to stdout, `echo` is like an +identity function and simply returns its arguments. When given no arguments, +it returns an empty string. When given one argument, it returns it as a +nushell value. Otherwise, it returns a list of the arguments. There is usually +little reason to use this over just writing the values as-is."# + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let args = call.rest(engine_state, stack, 0)?; + echo_impl(args, call.head) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let args = call.rest_const(working_set, 0)?; + echo_impl(args, call.head) + } + + fn is_const(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Put a list of numbers in the pipeline. This is the same as [1 2 3].", + example: "echo 1 2 3", + result: Some(Value::list( + vec![Value::test_int(1), Value::test_int(2), Value::test_int(3)], + Span::test_data(), + )), + }, + Example { + description: "Returns the piped-in value, by using the special $in variable to obtain it.", + example: "echo $in", + result: None, + }, + ] + } +} + +fn echo_impl(mut args: Vec, head: Span) -> Result { + let value = match args.len() { + 0 => Value::string("", head), + 1 => args.pop().expect("one element"), + _ => Value::list(args, head), + }; + Ok(value.into_pipeline_data()) +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Echo; + use crate::test_examples; + test_examples(Echo {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/error_make.rs b/nushell/crates/nu-cmd-lang/src/core_commands/error_make.rs new file mode 100644 index 0000000..a43d36f --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/error_make.rs @@ -0,0 +1,262 @@ +use nu_engine::command_prelude::*; +use nu_protocol::LabeledError; + +#[derive(Clone)] +pub struct ErrorMake; + +impl Command for ErrorMake { + fn name(&self) -> &str { + "error make" + } + + fn signature(&self) -> Signature { + Signature::build("error make") + .input_output_types(vec![(Type::Nothing, Type::Error)]) + .required( + "error_struct", + SyntaxShape::Record(vec![]), + "The error to create.", + ) + .switch( + "unspanned", + "remove the origin label from the error", + Some('u'), + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Create an error." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["panic", "crash", "throw"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let arg: Value = call.req(engine_state, stack, 0)?; + + let throw_span = if call.has_flag(engine_state, stack, "unspanned")? { + None + } else { + Some(call.head) + }; + + Err(make_other_error(&arg, throw_span)) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Create a simple custom error", + example: r#"error make {msg: "my custom error message"}"#, + result: None, + }, + Example { + description: "Create a more complex custom error", + example: r#"error make { + msg: "my custom error message" + label: { + text: "my custom label text" # not mandatory unless $.label exists + # optional + span: { + # if $.label.span exists, both start and end must be present + start: 123 + end: 456 + } + } + help: "A help string, suggesting a fix to the user" # optional + }"#, + result: None, + }, + Example { + description: "Create a custom error for a custom command that shows the span of the argument", + example: r#"def foo [x] { + error make { + msg: "this is fishy" + label: { + text: "fish right here" + span: (metadata $x).span + } + } + }"#, + result: None, + }, + ] + } +} + +const UNABLE_TO_PARSE: &str = "Unable to parse error format."; + +fn make_other_error(value: &Value, throw_span: Option) -> ShellError { + let span = value.span(); + let value = match value { + Value::Record { val, .. } => val, + _ => { + return ShellError::GenericError { + error: "Creating error value not supported.".into(), + msg: "unsupported error format, must be a record".into(), + span: throw_span, + help: None, + inner: vec![], + }; + } + }; + + let msg = match value.get("msg") { + Some(Value::String { val, .. }) => val.clone(), + Some(_) => { + return ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: "`$.msg` has wrong type, must be string".into(), + span: Some(span), + help: None, + inner: vec![], + }; + } + None => { + return ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: "missing required member `$.msg`".into(), + span: Some(span), + help: None, + inner: vec![], + }; + } + }; + + let help = match value.get("help") { + Some(Value::String { val, .. }) => Some(val.clone()), + _ => None, + }; + + let (label, label_span) = match value.get("label") { + Some(value @ Value::Record { val, .. }) => (val, value.span()), + Some(_) => { + return ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: "`$.label` has wrong type, must be a record".into(), + span: Some(span), + help: None, + inner: vec![], + }; + } + // correct return: no label + None => { + return ShellError::GenericError { + error: msg, + msg: "originates from here".into(), + span: throw_span, + help, + inner: vec![], + }; + } + }; + + // remove after a few versions + if label.get("start").is_some() || label.get("end").is_some() { + return ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: "`start` and `end` are deprecated".into(), + span: Some(span), + help: Some("Use `$.label.span` instead".into()), + inner: vec![], + }; + } + + let text = match label.get("text") { + Some(Value::String { val, .. }) => val.clone(), + Some(_) => { + return ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: "`$.label.text` has wrong type, must be string".into(), + span: Some(label_span), + help: None, + inner: vec![], + }; + } + None => { + return ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: "missing required member `$.label.text`".into(), + span: Some(label_span), + help: None, + inner: vec![], + }; + } + }; + + let (span, span_span) = match label.get("span") { + Some(value @ Value::Record { val, .. }) => (val, value.span()), + Some(value) => { + return ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: "`$.label.span` has wrong type, must be record".into(), + span: Some(value.span()), + help: None, + inner: vec![], + }; + } + // correct return: label, no span + None => { + return ShellError::GenericError { + error: msg, + msg: text, + span: throw_span, + help, + inner: vec![], + }; + } + }; + + let span_start = match get_span_sides(span, span_span, "start") { + Ok(val) => val, + Err(err) => return err, + }; + let span_end = match get_span_sides(span, span_span, "end") { + Ok(val) => val, + Err(err) => return err, + }; + + if span_start > span_end { + return ShellError::GenericError { + error: "invalid error format.".into(), + msg: "`$.label.start` should be smaller than `$.label.end`".into(), + span: Some(label_span), + help: Some(format!("{} > {}", span_start, span_end)), + inner: vec![], + }; + } + + // correct return: everything present + let mut error = + LabeledError::new(msg).with_label(text, Span::new(span_start as usize, span_end as usize)); + error.help = help; + error.into() +} + +fn get_span_sides(span: &Record, span_span: Span, side: &str) -> Result { + match span.get(side) { + Some(Value::Int { val, .. }) => Ok(*val), + Some(_) => Err(ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: format!("`$.span.{side}` must be int"), + span: Some(span_span), + help: None, + inner: vec![], + }), + None => Err(ShellError::GenericError { + error: UNABLE_TO_PARSE.into(), + msg: format!("`$.span.{side}` must be present, if span is specified."), + span: Some(span_span), + help: None, + inner: vec![], + }), + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/export.rs b/nushell/crates/nu-cmd-lang/src/core_commands/export.rs new file mode 100644 index 0000000..d7e923d --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/export.rs @@ -0,0 +1,52 @@ +use nu_engine::{command_prelude::*, get_full_help}; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct ExportCommand; + +impl Command for ExportCommand { + fn name(&self) -> &str { + "export" + } + + fn signature(&self) -> Signature { + Signature::build("export") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Export definitions or environment variables from a module." + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Export a definition from a module", + example: r#"module utils { export def my-command [] { "hello" } }; use utils my-command; my-command"#, + result: Some(Value::test_string("hello")), + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["module"] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/export_alias.rs b/nushell/crates/nu-cmd-lang/src/core_commands/export_alias.rs new file mode 100644 index 0000000..a3001cd --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/export_alias.rs @@ -0,0 +1,58 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct ExportAlias; + +impl Command for ExportAlias { + fn name(&self) -> &str { + "export alias" + } + + fn description(&self) -> &str { + "Alias a command (with optional flags) to a new name and export it from a module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export alias") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("name", SyntaxShape::String, "Name of the alias.") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)), + "Equals sign followed by value.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn search_terms(&self) -> Vec<&str> { + vec!["abbr", "aka", "fn", "func", "function"] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Alias ll to ls -l and export it from a module", + example: "module spam { export alias ll = ls -l }", + result: Some(Value::nothing(Span::test_data())), + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/export_const.rs b/nushell/crates/nu-cmd-lang/src/core_commands/export_const.rs new file mode 100644 index 0000000..59e9385 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/export_const.rs @@ -0,0 +1,63 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct ExportConst; + +impl Command for ExportConst { + fn name(&self) -> &str { + "export const" + } + + fn description(&self) -> &str { + "Use parse-time constant from a module and export them from this module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export const") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("const_name", SyntaxShape::VarWithOptType, "Constant name.") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), + "Equals sign followed by constant value.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Re-export a command from another module", + example: r#"module spam { export const foo = 3; } + module eggs { export use spam foo } + use eggs foo + foo + "#, + result: Some(Value::test_int(3)), + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["reexport", "import", "module"] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/export_def.rs b/nushell/crates/nu-cmd-lang/src/core_commands/export_def.rs new file mode 100644 index 0000000..51c4c87 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/export_def.rs @@ -0,0 +1,57 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct ExportDef; + +impl Command for ExportDef { + fn name(&self) -> &str { + "export def" + } + + fn description(&self) -> &str { + "Define a custom command and export it from a module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export def") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("def_name", SyntaxShape::String, "Command name.") + .required("params", SyntaxShape::Signature, "Parameters.") + .required("block", SyntaxShape::Block, "Body of the definition.") + .switch("env", "keep the environment defined inside the command", None) + .switch("wrapped", "treat unknown flags and arguments as strings (requires ...rest-like parameter in signature)", None) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Define a custom command in a module and call it", + example: r#"module spam { export def foo [] { "foo" } }; use spam foo; foo"#, + result: Some(Value::test_string("foo")), + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["module"] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/export_extern.rs b/nushell/crates/nu-cmd-lang/src/core_commands/export_extern.rs new file mode 100644 index 0000000..580e503 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/export_extern.rs @@ -0,0 +1,54 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct ExportExtern; + +impl Command for ExportExtern { + fn name(&self) -> &str { + "export extern" + } + + fn description(&self) -> &str { + "Define an extern and export it from a module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export extern") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("def_name", SyntaxShape::String, "Definition name.") + .required("params", SyntaxShape::Signature, "Parameters.") + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Export the signature for an external command", + example: r#"export extern echo [text: string]"#, + result: None, + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["signature", "module", "declare"] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/export_module.rs b/nushell/crates/nu-cmd-lang/src/core_commands/export_module.rs new file mode 100644 index 0000000..c749c1a --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/export_module.rs @@ -0,0 +1,72 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct ExportModule; + +impl Command for ExportModule { + fn name(&self) -> &str { + "export module" + } + + fn description(&self) -> &str { + "Export a custom module from a module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export module") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("module", SyntaxShape::String, "Module name or module path.") + .optional( + "block", + SyntaxShape::Block, + "Body of the module if 'module' parameter is not a path.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Define a custom command in a submodule of a module and call it", + example: r#"module spam { + export module eggs { + export def foo [] { "foo" } + } + } + use spam eggs + eggs foo"#, + result: Some(Value::test_string("foo")), + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ExportModule {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/export_use.rs b/nushell/crates/nu-cmd-lang/src/core_commands/export_use.rs new file mode 100644 index 0000000..4d2c3bf --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/export_use.rs @@ -0,0 +1,62 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct ExportUse; + +impl Command for ExportUse { + fn name(&self) -> &str { + "export use" + } + + fn description(&self) -> &str { + "Use definitions from a module and export them from this module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export use") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("module", SyntaxShape::String, "Module or module file.") + .rest( + "members", + SyntaxShape::Any, + "Which members of the module to import.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Re-export a command from another module", + example: r#"module spam { export def foo [] { "foo" } } + module eggs { export use spam foo } + use eggs foo + foo + "#, + result: Some(Value::test_string("foo")), + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["reexport", "import", "module"] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/extern_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/extern_.rs new file mode 100644 index 0000000..c0afcdf --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/extern_.rs @@ -0,0 +1,50 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Extern; + +impl Command for Extern { + fn name(&self) -> &str { + "extern" + } + + fn description(&self) -> &str { + "Define a signature for an external command." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("extern") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("def_name", SyntaxShape::String, "Definition name.") + .required("params", SyntaxShape::Signature, "Parameters.") + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Write a signature for an external command", + example: r#"extern echo [text: string]"#, + result: None, + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/for_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/for_.rs new file mode 100644 index 0000000..1798650 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/for_.rs @@ -0,0 +1,90 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct For; + +impl Command for For { + fn name(&self) -> &str { + "for" + } + + fn description(&self) -> &str { + "Loop over a range." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("for") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required( + "var_name", + SyntaxShape::VarWithOptType, + "Name of the looping variable.", + ) + .required( + "range", + SyntaxShape::Keyword(b"in".to_vec(), Box::new(SyntaxShape::Any)), + "Range of the loop.", + ) + .required("block", SyntaxShape::Block, "The block to run.") + .creates_scope() + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'for' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Print the square of each integer", + example: "for x in [1 2 3] { print ($x * $x) }", + result: None, + }, + Example { + description: "Work with elements of a range", + example: "for $x in 1..3 { print $x }", + result: None, + }, + Example { + description: "Number each item and print a message", + example: r#"for $it in (['bob' 'fred'] | enumerate) { print $"($it.index) is ($it.item)" }"#, + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(For {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/hide.rs b/nushell/crates/nu-cmd-lang/src/core_commands/hide.rs new file mode 100644 index 0000000..8524f26 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/hide.rs @@ -0,0 +1,67 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Hide; + +impl Command for Hide { + fn name(&self) -> &str { + "hide" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("hide") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("module", SyntaxShape::String, "Module or module file.") + .optional( + "members", + SyntaxShape::Any, + "Which members of the module to import.", + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Hide definitions in the current scope." + } + + fn extra_description(&self) -> &str { + r#"Definitions are hidden by priority: First aliases, then custom commands. + +This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn search_terms(&self) -> Vec<&str> { + vec!["unset"] + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Hide the alias just defined", + example: r#"alias lll = ls -l; hide lll"#, + result: None, + }, + Example { + description: "Hide a custom command", + example: r#"def say-hi [] { echo 'Hi!' }; hide say-hi"#, + result: None, + }, + ] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/hide_env.rs b/nushell/crates/nu-cmd-lang/src/core_commands/hide_env.rs new file mode 100644 index 0000000..857343e --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/hide_env.rs @@ -0,0 +1,74 @@ +use nu_engine::command_prelude::*; +use nu_protocol::did_you_mean; + +#[derive(Clone)] +pub struct HideEnv; + +impl Command for HideEnv { + fn name(&self) -> &str { + "hide-env" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("hide-env") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .rest( + "name", + SyntaxShape::String, + "Environment variable names to hide.", + ) + .switch( + "ignore-errors", + "do not throw an error if an environment variable was not found", + Some('i'), + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Hide environment variables in the current scope." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["unset", "drop"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let env_var_names: Vec> = call.rest(engine_state, stack, 0)?; + let ignore_errors = call.has_flag(engine_state, stack, "ignore-errors")?; + + for name in env_var_names { + if !stack.remove_env_var(engine_state, &name.item) && !ignore_errors { + let all_names = stack.get_env_var_names(engine_state); + if let Some(closest_match) = did_you_mean(&all_names, &name.item) { + return Err(ShellError::DidYouMeanCustom { + msg: format!("Environment variable '{}' not found", name.item), + suggestion: closest_match, + span: name.span, + }); + } else { + return Err(ShellError::EnvVarNotFoundAtRuntime { + envvar_name: name.item, + span: name.span, + }); + } + } + } + + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Hide an environment variable", + example: r#"$env.HZ_ENV_ABC = 1; hide-env HZ_ENV_ABC; 'HZ_ENV_ABC' in $env"#, + result: Some(Value::test_bool(false)), + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/if_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/if_.rs new file mode 100644 index 0000000..7c54ca4 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/if_.rs @@ -0,0 +1,144 @@ +use nu_engine::command_prelude::*; +use nu_protocol::{ + engine::{CommandType, StateWorkingSet}, + eval_const::{eval_const_subexpression, eval_constant, eval_constant_with_input}, +}; + +#[derive(Clone)] +pub struct If; + +impl Command for If { + fn name(&self) -> &str { + "if" + } + + fn description(&self) -> &str { + "Conditionally run a block." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("if") + .input_output_types(vec![(Type::Any, Type::Any)]) + .required("cond", SyntaxShape::MathExpression, "Condition to check.") + .required( + "then_block", + SyntaxShape::Block, + "Block to run if check succeeds.", + ) + .optional( + "else_expression", + SyntaxShape::Keyword( + b"else".to_vec(), + Box::new(SyntaxShape::OneOf(vec![ + SyntaxShape::Block, + SyntaxShape::Expression, + ])), + ), + "Expression or block to run when the condition is false.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn is_const(&self) -> bool { + true + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let call = call.assert_ast_call()?; + let cond = call.positional_nth(0).expect("checked through parser"); + let then_block = call + .positional_nth(1) + .expect("checked through parser") + .as_block() + .expect("internal error: missing block"); + let else_case = call.positional_nth(2); + + if eval_constant(working_set, cond)?.as_bool()? { + let block = working_set.get_block(then_block); + eval_const_subexpression(working_set, block, input, block.span.unwrap_or(call.head)) + } else if let Some(else_case) = else_case { + if let Some(else_expr) = else_case.as_keyword() { + if let Some(block_id) = else_expr.as_block() { + let block = working_set.get_block(block_id); + eval_const_subexpression( + working_set, + block, + input, + block.span.unwrap_or(call.head), + ) + } else { + eval_constant_with_input(working_set, else_expr, input) + } + } else { + eval_constant_with_input(working_set, else_case, input) + } + } else { + Ok(PipelineData::empty()) + } + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'if' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn search_terms(&self) -> Vec<&str> { + vec!["else", "conditional"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Output a value if a condition matches, otherwise return nothing", + example: "if 2 < 3 { 'yes!' }", + result: Some(Value::test_string("yes!")), + }, + Example { + description: "Output a value if a condition matches, else return another value", + example: "if 5 < 3 { 'yes!' } else { 'no!' }", + result: Some(Value::test_string("no!")), + }, + Example { + description: "Chain multiple if's together", + example: "if 5 < 3 { 'yes!' } else if 4 < 5 { 'no!' } else { 'okay!' }", + result: Some(Value::test_string("no!")), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(If {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/ignore.rs b/nushell/crates/nu-cmd-lang/src/core_commands/ignore.rs new file mode 100644 index 0000000..e4810e0 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/ignore.rs @@ -0,0 +1,78 @@ +use nu_engine::command_prelude::*; +use nu_protocol::{ByteStreamSource, OutDest, engine::StateWorkingSet}; + +#[derive(Clone)] +pub struct Ignore; + +impl Command for Ignore { + fn name(&self) -> &str { + "ignore" + } + + fn description(&self) -> &str { + "Ignore the output of the previous command in the pipeline." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("ignore") + .input_output_types(vec![(Type::Any, Type::Nothing)]) + .category(Category::Core) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["silent", "quiet", "out-null"] + } + + fn is_const(&self) -> bool { + true + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + mut input: PipelineData, + ) -> Result { + if let PipelineData::ByteStream(stream, _) = &mut input { + #[cfg(feature = "os")] + if let ByteStreamSource::Child(child) = stream.source_mut() { + child.ignore_error(true); + } + } + input.drain()?; + Ok(PipelineData::empty()) + } + + fn run_const( + &self, + _working_set: &StateWorkingSet, + _call: &Call, + input: PipelineData, + ) -> Result { + input.drain()?; + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Ignore the output of an echo command", + example: "echo done | ignore", + result: Some(Value::nothing(Span::test_data())), + }] + } + + fn pipe_redirection(&self) -> (Option, Option) { + (Some(OutDest::Null), None) + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Ignore; + use crate::test_examples; + test_examples(Ignore {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/let_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/let_.rs new file mode 100644 index 0000000..d34badf --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/let_.rs @@ -0,0 +1,95 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Let; + +impl Command for Let { + fn name(&self) -> &str { + "let" + } + + fn description(&self) -> &str { + "Create a variable and give it a value." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("let") + .input_output_types(vec![(Type::Any, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("var_name", SyntaxShape::VarWithOptType, "Variable name.") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), + "Equals sign followed by value.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn search_terms(&self) -> Vec<&str> { + vec!["set", "const"] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'let' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Set a variable to a value", + example: "let x = 10", + result: None, + }, + Example { + description: "Set a variable to the result of an expression", + example: "let x = 10 + 100", + result: None, + }, + Example { + description: "Set a variable based on the condition", + example: "let x = if false { -1 } else { 1 }", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use nu_protocol::engine::CommandType; + + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Let {}) + } + + #[test] + fn test_command_type() { + assert!(matches!(Let.command_type(), CommandType::Keyword)); + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/loop_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/loop_.rs new file mode 100644 index 0000000..502fbdb --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/loop_.rs @@ -0,0 +1,67 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Loop; + +impl Command for Loop { + fn name(&self) -> &str { + "loop" + } + + fn description(&self) -> &str { + "Run a block in a loop." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("loop") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("block", SyntaxShape::Block, "Block to loop.") + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'loop' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Loop while a condition is true", + example: "mut x = 0; loop { if $x > 10 { break }; $x = $x + 1 }; $x", + result: Some(Value::test_int(11)), + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Loop {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/match_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/match_.rs new file mode 100644 index 0000000..677dcb0 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/match_.rs @@ -0,0 +1,112 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Match; + +impl Command for Match { + fn name(&self) -> &str { + "match" + } + + fn description(&self) -> &str { + "Conditionally run a block on a matched value." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("match") + .input_output_types(vec![(Type::Any, Type::Any)]) + .required("value", SyntaxShape::Any, "Value to check.") + .required( + "match_block", + SyntaxShape::MatchBlock, + "Block to run if check succeeds.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'match' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Match on a value", + example: "match 3 { 1 => 'one', 2 => 'two', 3 => 'three' }", + result: Some(Value::test_string("three")), + }, + Example { + description: "Match against alternative values", + example: "match 'three' { 1 | 'one' => '-', 2 | 'two' => '--', 3 | 'three' => '---' }", + result: Some(Value::test_string("---")), + }, + Example { + description: "Match on a value in range", + example: "match 3 { 1..10 => 'yes!' }", + result: Some(Value::test_string("yes!")), + }, + Example { + description: "Match on a field in a record", + example: "match {a: 100} { {a: $my_value} => { $my_value } }", + result: Some(Value::test_int(100)), + }, + Example { + description: "Match with a catch-all", + example: "match 3 { 1 => { 'yes!' }, _ => { 'no!' } }", + result: Some(Value::test_string("no!")), + }, + Example { + description: "Match against a list", + example: "match [1, 2, 3] { [$a, $b, $c] => { $a + $b + $c }, _ => 0 }", + result: Some(Value::test_int(6)), + }, + Example { + description: "Match against pipeline input", + example: "{a: {b: 3}} | match $in {{a: { $b }} => ($b + 10) }", + result: Some(Value::test_int(13)), + }, + Example { + description: "Match with a guard", + example: "match [1 2 3] { + [$x, ..$y] if $x == 1 => { 'good list' }, + _ => { 'not a very good list' } + } + ", + result: Some(Value::test_string("good list")), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Match {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/mod.rs b/nushell/crates/nu-cmd-lang/src/core_commands/mod.rs new file mode 100644 index 0000000..dd54fff --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/mod.rs @@ -0,0 +1,73 @@ +mod alias; +mod attr; +mod break_; +mod collect; +mod const_; +mod continue_; +mod def; +mod describe; +mod do_; +mod echo; +mod error_make; +mod export; +mod export_alias; +mod export_const; +mod export_def; +mod export_extern; +mod export_module; +mod export_use; +mod extern_; +mod for_; +mod hide; +mod hide_env; +mod if_; +mod ignore; +mod let_; +mod loop_; +mod match_; +mod module; +mod mut_; +pub(crate) mod overlay; +mod return_; +mod scope; +mod try_; +mod use_; +mod version; +mod while_; + +pub use alias::Alias; +pub use attr::*; +pub use break_::Break; +pub use collect::Collect; +pub use const_::Const; +pub use continue_::Continue; +pub use def::Def; +pub use describe::Describe; +pub use do_::Do; +pub use echo::Echo; +pub use error_make::ErrorMake; +pub use export::ExportCommand; +pub use export_alias::ExportAlias; +pub use export_const::ExportConst; +pub use export_def::ExportDef; +pub use export_extern::ExportExtern; +pub use export_module::ExportModule; +pub use export_use::ExportUse; +pub use extern_::Extern; +pub use for_::For; +pub use hide::Hide; +pub use hide_env::HideEnv; +pub use if_::If; +pub use ignore::Ignore; +pub use let_::Let; +pub use loop_::Loop; +pub use match_::Match; +pub use module::Module; +pub use mut_::Mut; +pub use overlay::*; +pub use return_::Return; +pub use scope::*; +pub use try_::Try; +pub use use_::Use; +pub use version::{VERSION_NU_FEATURES, Version}; +pub use while_::While; diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/module.rs b/nushell/crates/nu-cmd-lang/src/core_commands/module.rs new file mode 100644 index 0000000..4ed2d5c --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/module.rs @@ -0,0 +1,67 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Module; + +impl Command for Module { + fn name(&self) -> &str { + "module" + } + + fn description(&self) -> &str { + "Define a custom module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("module") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("module", SyntaxShape::String, "Module name or module path.") + .optional( + "block", + SyntaxShape::Block, + "Body of the module if 'module' parameter is not a module path.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Define a custom command in a module and call it", + example: r#"module spam { export def foo [] { "foo" } }; use spam foo; foo"#, + result: Some(Value::test_string("foo")), + }, + Example { + description: "Define an environment variable in a module", + example: r#"module foo { export-env { $env.FOO = "BAZ" } }; use foo; $env.FOO"#, + result: Some(Value::test_string("BAZ")), + }, + Example { + description: "Define a custom command that participates in the environment in a module and call it", + example: r#"module foo { export def --env bar [] { $env.FOO_BAR = "BAZ" } }; use foo bar; bar; $env.FOO_BAR"#, + result: Some(Value::test_string("BAZ")), + }, + ] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/mut_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/mut_.rs new file mode 100644 index 0000000..85cff33 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/mut_.rs @@ -0,0 +1,100 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Mut; + +impl Command for Mut { + fn name(&self) -> &str { + "mut" + } + + fn description(&self) -> &str { + "Create a mutable variable and give it a value." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("mut") + .input_output_types(vec![(Type::Any, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("var_name", SyntaxShape::VarWithOptType, "Variable name.") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), + "Equals sign followed by value.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn search_terms(&self) -> Vec<&str> { + vec!["set", "mutable"] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'mut' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Set a mutable variable to a value, then update it", + example: "mut x = 10; $x = 12", + result: None, + }, + Example { + description: "Upsert a value inside a mutable data structure", + example: "mut a = {b:{c:1}}; $a.b.c = 2", + result: None, + }, + Example { + description: "Set a mutable variable to the result of an expression", + example: "mut x = 10 + 100", + result: None, + }, + Example { + description: "Set a mutable variable based on the condition", + example: "mut x = if false { -1 } else { 1 }", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use nu_protocol::engine::CommandType; + + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Mut {}) + } + + #[test] + fn test_command_type() { + assert!(matches!(Mut.command_type(), CommandType::Keyword)); + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/overlay/command.rs b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/command.rs new file mode 100644 index 0000000..c39df28 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/command.rs @@ -0,0 +1,42 @@ +use nu_engine::{command_prelude::*, get_full_help}; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Overlay; + +impl Command for Overlay { + fn name(&self) -> &str { + "overlay" + } + + fn signature(&self) -> Signature { + Signature::build("overlay") + .category(Category::Core) + .input_output_types(vec![(Type::Nothing, Type::String)]) + } + + fn description(&self) -> &str { + "Commands for manipulating overlays." + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html + + You must use one of the following subcommands. Using this command as-is will only produce this help message."# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/overlay/hide.rs b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/hide.rs new file mode 100644 index 0000000..666fede --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/hide.rs @@ -0,0 +1,137 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct OverlayHide; + +impl Command for OverlayHide { + fn name(&self) -> &str { + "overlay hide" + } + + fn description(&self) -> &str { + "Hide an active overlay." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("overlay hide") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .optional("name", SyntaxShape::String, "Overlay to hide.") + .switch( + "keep-custom", + "Keep all newly added commands and aliases in the next activated overlay.", + Some('k'), + ) + .named( + "keep-env", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "List of environment variables to keep in the next activated overlay", + Some('e'), + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let overlay_name: Spanned = if let Some(name) = call.opt(engine_state, stack, 0)? { + name + } else { + Spanned { + item: stack.last_overlay_name()?, + span: call.head, + } + }; + + if !stack.is_overlay_active(&overlay_name.item) { + return Err(ShellError::OverlayNotFoundAtRuntime { + overlay_name: overlay_name.item, + span: overlay_name.span, + }); + } + + let keep_env: Option>> = + call.get_flag(engine_state, stack, "keep-env")?; + + let env_vars_to_keep = if let Some(env_var_names_to_keep) = keep_env { + let mut env_vars_to_keep = vec![]; + + for name in env_var_names_to_keep.into_iter() { + match stack.get_env_var(engine_state, &name.item) { + Some(val) => env_vars_to_keep.push((name.item, val.clone())), + None => { + return Err(ShellError::EnvVarNotFoundAtRuntime { + envvar_name: name.item, + span: name.span, + }); + } + } + } + + env_vars_to_keep + } else { + vec![] + }; + + // also restore env vars which has been hidden + let env_vars_to_restore = stack.get_hidden_env_vars(&overlay_name.item, engine_state); + stack.remove_overlay(&overlay_name.item); + for (name, val) in env_vars_to_restore { + stack.add_env_var(name, val); + } + + for (name, val) in env_vars_to_keep { + stack.add_env_var(name, val); + } + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Keep a custom command after hiding the overlay", + example: r#"module spam { export def foo [] { "foo" } } + overlay use spam + def bar [] { "bar" } + overlay hide spam --keep-custom + bar + "#, + result: None, + }, + Example { + description: "Hide an overlay created from a file", + example: r#"'export alias f = "foo"' | save spam.nu + overlay use spam.nu + overlay hide spam"#, + result: None, + }, + Example { + description: "Hide the last activated overlay", + example: r#"module spam { export-env { $env.FOO = "foo" } } + overlay use spam + overlay hide"#, + result: None, + }, + Example { + description: "Keep the current working directory when removing an overlay", + example: r#"overlay new spam + cd some-dir + overlay hide --keep-env [ PWD ] spam"#, + result: None, + }, + ] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/overlay/list.rs b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/list.rs new file mode 100644 index 0000000..d646d63 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/list.rs @@ -0,0 +1,50 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct OverlayList; + +impl Command for OverlayList { + fn name(&self) -> &str { + "overlay list" + } + + fn description(&self) -> &str { + "List all active overlays." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("overlay list") + .category(Category::Core) + .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::String)))]) + } + + fn extra_description(&self) -> &str { + "The overlays are listed in the order they were activated." + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let active_overlays_engine: Vec = stack + .active_overlays + .iter() + .map(|s| Value::string(s, call.head)) + .collect(); + + Ok(Value::list(active_overlays_engine, call.head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the last activated overlay", + example: r#"module spam { export def foo [] { "foo" } } + overlay use spam + overlay list | last"#, + result: Some(Value::test_string("spam")), + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/overlay/mod.rs b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/mod.rs new file mode 100644 index 0000000..23ca1f9 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/mod.rs @@ -0,0 +1,11 @@ +mod command; +mod hide; +mod list; +mod new; +mod use_; + +pub use command::Overlay; +pub use hide::OverlayHide; +pub use list::OverlayList; +pub use new::OverlayNew; +pub use use_::OverlayUse; diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/overlay/new.rs b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/new.rs new file mode 100644 index 0000000..fa1acbc --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/new.rs @@ -0,0 +1,86 @@ +use nu_engine::{command_prelude::*, redirect_env}; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct OverlayNew; + +impl Command for OverlayNew { + fn name(&self) -> &str { + "overlay new" + } + + fn description(&self) -> &str { + "Create an empty overlay." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("overlay new") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("name", SyntaxShape::String, "Name of the overlay.") + .switch( + "reload", + "If the overlay already exists, reload its environment.", + Some('r'), + ) + // TODO: + // .switch( + // "prefix", + // "Prepend module name to the imported symbols", + // Some('p'), + // ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"The command will first create an empty module, then add it as an overlay. + +This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + engine_state: &EngineState, + caller_stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let name_arg: Spanned = call.req(engine_state, caller_stack, 0)?; + let reload = call.has_flag(engine_state, caller_stack, "reload")?; + + if reload { + let callee_stack = caller_stack.clone(); + caller_stack.add_overlay(name_arg.item); + redirect_env(engine_state, caller_stack, &callee_stack); + } else { + caller_stack.add_overlay(name_arg.item); + } + + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create an empty overlay", + example: r#"overlay new spam"#, + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(OverlayNew {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs new file mode 100644 index 0000000..bf29087 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs @@ -0,0 +1,230 @@ +use nu_engine::{ + command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block, redirect_env, +}; +use nu_parser::trim_quotes_str; +use nu_protocol::{ModuleId, ast::Expr, engine::CommandType}; + +use std::path::Path; + +#[derive(Clone)] +pub struct OverlayUse; + +impl Command for OverlayUse { + fn name(&self) -> &str { + "overlay use" + } + + fn description(&self) -> &str { + "Use definitions from a module as an overlay." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("overlay use") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required( + "name", + SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]), + "Module name to use overlay for (`null` for no-op).", + ) + .optional( + "as", + SyntaxShape::Keyword(b"as".to_vec(), Box::new(SyntaxShape::String)), + "`as` keyword followed by a new name.", + ) + .switch( + "prefix", + "Prepend module name to the imported commands and aliases", + Some('p'), + ) + .switch( + "reload", + "If the overlay already exists, reload its definitions and environment.", + Some('r'), + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + engine_state: &EngineState, + caller_stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let noop = call.get_parser_info(caller_stack, "noop"); + if noop.is_some() { + return Ok(PipelineData::empty()); + } + + let mut name_arg: Spanned = call.req(engine_state, caller_stack, 0)?; + name_arg.item = trim_quotes_str(&name_arg.item).to_string(); + + let maybe_origin_module_id: Option = + if let Some(overlay_expr) = call.get_parser_info(caller_stack, "overlay_expr") { + if let Expr::Overlay(module_id) = &overlay_expr.expr { + *module_id + } else { + return Err(ShellError::NushellFailedSpanned { + msg: "Not an overlay".to_string(), + label: "requires an overlay (path or a string)".to_string(), + span: overlay_expr.span, + }); + } + } else { + return Err(ShellError::NushellFailedSpanned { + msg: "Missing positional".to_string(), + label: "missing required overlay".to_string(), + span: call.head, + }); + }; + + let overlay_name = if let Some(name) = call.opt(engine_state, caller_stack, 1)? { + name + } else if engine_state + .find_overlay(name_arg.item.as_bytes()) + .is_some() + { + name_arg.item.clone() + } else if let Some(os_str) = Path::new(&name_arg.item).file_stem() { + if let Some(name) = os_str.to_str() { + name.to_string() + } else { + return Err(ShellError::NonUtf8 { + span: name_arg.span, + }); + } + } else { + return Err(ShellError::OverlayNotFoundAtRuntime { + overlay_name: name_arg.item, + span: name_arg.span, + }); + }; + + if let Some(module_id) = maybe_origin_module_id { + // Add environment variables only if (determined by parser): + // a) adding a new overlay + // b) refreshing an active overlay (the origin module changed) + + let module = engine_state.get_module(module_id); + // in such case, should also make sure that PWD is not restored in old overlays. + let cwd = caller_stack.get_env_var(engine_state, "PWD").cloned(); + + // Evaluate the export-env block (if any) and keep its environment + if let Some(block_id) = module.env_block { + let maybe_file_path_or_dir = find_in_dirs_env( + &name_arg.item, + engine_state, + caller_stack, + get_dirs_var_from_call(caller_stack, call), + )?; + let block = engine_state.get_block(block_id); + let mut callee_stack = caller_stack + .gather_captures(engine_state, &block.captures) + .reset_pipes(); + + if let Some(path) = &maybe_file_path_or_dir { + // Set the currently evaluated directory, if the argument is a valid path + let parent = if path.is_dir() { + path.clone() + } else { + let mut parent = path.clone(); + parent.pop(); + parent + }; + let file_pwd = Value::string(parent.to_string_lossy(), call.head); + + callee_stack.add_env_var("FILE_PWD".to_string(), file_pwd); + } + + if let Some(path) = &maybe_file_path_or_dir { + let module_file_path = if path.is_dir() { + // the existence of `mod.nu` is verified in parsing time + // so it's safe to use it here. + Value::string(path.join("mod.nu").to_string_lossy(), call.head) + } else { + Value::string(path.to_string_lossy(), call.head) + }; + callee_stack.add_env_var("CURRENT_FILE".to_string(), module_file_path); + } + + let eval_block = get_eval_block(engine_state); + let _ = eval_block(engine_state, &mut callee_stack, block, input); + + // The export-env block should see the env vars *before* activating this overlay + caller_stack.add_overlay(overlay_name); + // make sure that PWD is not restored in old overlays. + if let Some(cwd) = cwd { + caller_stack.add_env_var("PWD".to_string(), cwd); + } + + // Merge the block's environment to the current stack + redirect_env(engine_state, caller_stack, &callee_stack); + } else { + caller_stack.add_overlay(overlay_name); + // make sure that PWD is not restored in old overlays. + if let Some(cwd) = cwd { + caller_stack.add_env_var("PWD".to_string(), cwd); + } + } + } else { + caller_stack.add_overlay(overlay_name); + } + + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Create an overlay from a module", + example: r#"module spam { export def foo [] { "foo" } } + overlay use spam + foo"#, + result: None, + }, + Example { + description: "Create an overlay from a module and rename it", + example: r#"module spam { export def foo [] { "foo" } } + overlay use spam as spam_new + foo"#, + result: None, + }, + Example { + description: "Create an overlay with a prefix", + example: r#"'export def foo { "foo" }' + overlay use --prefix spam + spam foo"#, + result: None, + }, + Example { + description: "Create an overlay from a file", + example: r#"'export-env { $env.FOO = "foo" }' | save spam.nu + overlay use spam.nu + $env.FOO"#, + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(OverlayUse {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/return_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/return_.rs new file mode 100644 index 0000000..72a46d1 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/return_.rs @@ -0,0 +1,58 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Return; + +impl Command for Return { + fn name(&self) -> &str { + "return" + } + + fn description(&self) -> &str { + "Return early from a custom command." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("return") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .optional( + "return_value", + SyntaxShape::Any, + "Optional value to return.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'return' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Return early", + example: r#"def foo [] { return }"#, + result: None, + }] + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/scope/aliases.rs b/nushell/crates/nu-cmd-lang/src/core_commands/scope/aliases.rs new file mode 100644 index 0000000..6e5290a --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/scope/aliases.rs @@ -0,0 +1,54 @@ +use nu_engine::{command_prelude::*, scope::ScopeData}; + +#[derive(Clone)] +pub struct ScopeAliases; + +impl Command for ScopeAliases { + fn name(&self) -> &str { + "scope aliases" + } + + fn signature(&self) -> Signature { + Signature::build("scope aliases") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .allow_variants_without_examples(true) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Output info on the aliases in the current scope." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let mut scope_data = ScopeData::new(engine_state, stack); + scope_data.populate_decls(); + Ok(Value::list(scope_data.collect_aliases(head), head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Show the aliases in the current scope", + example: "scope aliases", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ScopeAliases {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/scope/command.rs b/nushell/crates/nu-cmd-lang/src/core_commands/scope/command.rs new file mode 100644 index 0000000..5e252c0 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/scope/command.rs @@ -0,0 +1,31 @@ +use nu_engine::{command_prelude::*, get_full_help}; + +#[derive(Clone)] +pub struct Scope; + +impl Command for Scope { + fn name(&self) -> &str { + "scope" + } + + fn signature(&self) -> Signature { + Signature::build("scope") + .category(Category::Core) + .input_output_types(vec![(Type::Nothing, Type::String)]) + .allow_variants_without_examples(true) + } + + fn description(&self) -> &str { + "Commands for getting info about what is in scope." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/scope/commands.rs b/nushell/crates/nu-cmd-lang/src/core_commands/scope/commands.rs new file mode 100644 index 0000000..a1fcd7b --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/scope/commands.rs @@ -0,0 +1,54 @@ +use nu_engine::{command_prelude::*, scope::ScopeData}; + +#[derive(Clone)] +pub struct ScopeCommands; + +impl Command for ScopeCommands { + fn name(&self) -> &str { + "scope commands" + } + + fn signature(&self) -> Signature { + Signature::build("scope commands") + .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::Any)))]) + .allow_variants_without_examples(true) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Output info on the commands in the current scope." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let mut scope_data = ScopeData::new(engine_state, stack); + scope_data.populate_decls(); + Ok(Value::list(scope_data.collect_commands(head), head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Show the commands in the current scope", + example: "scope commands", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ScopeCommands {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/scope/engine_stats.rs b/nushell/crates/nu-cmd-lang/src/core_commands/scope/engine_stats.rs new file mode 100644 index 0000000..3109ef2 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/scope/engine_stats.rs @@ -0,0 +1,55 @@ +use nu_engine::{command_prelude::*, scope::ScopeData}; + +#[derive(Clone)] +pub struct ScopeEngineStats; + +impl Command for ScopeEngineStats { + fn name(&self) -> &str { + "scope engine-stats" + } + + fn signature(&self) -> Signature { + Signature::build("scope engine-stats") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .allow_variants_without_examples(true) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Output stats on the engine in the current state." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let span = call.head; + + let scope_data = ScopeData::new(engine_state, stack); + + Ok(scope_data.collect_engine_state(span).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Show the stats on the current engine state", + example: "scope engine-stats", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ScopeEngineStats {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/scope/externs.rs b/nushell/crates/nu-cmd-lang/src/core_commands/scope/externs.rs new file mode 100644 index 0000000..fc7980c --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/scope/externs.rs @@ -0,0 +1,54 @@ +use nu_engine::{command_prelude::*, scope::ScopeData}; + +#[derive(Clone)] +pub struct ScopeExterns; + +impl Command for ScopeExterns { + fn name(&self) -> &str { + "scope externs" + } + + fn signature(&self) -> Signature { + Signature::build("scope externs") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .allow_variants_without_examples(true) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Output info on the known externals in the current scope." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let mut scope_data = ScopeData::new(engine_state, stack); + scope_data.populate_decls(); + Ok(Value::list(scope_data.collect_externs(head), head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Show the known externals in the current scope", + example: "scope externs", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ScopeExterns {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/scope/mod.rs b/nushell/crates/nu-cmd-lang/src/core_commands/scope/mod.rs new file mode 100644 index 0000000..1a65812 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/scope/mod.rs @@ -0,0 +1,15 @@ +mod aliases; +mod command; +mod commands; +mod engine_stats; +mod externs; +mod modules; +mod variables; + +pub use aliases::*; +pub use command::*; +pub use commands::*; +pub use engine_stats::*; +pub use externs::*; +pub use modules::*; +pub use variables::*; diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/scope/modules.rs b/nushell/crates/nu-cmd-lang/src/core_commands/scope/modules.rs new file mode 100644 index 0000000..1f5ca9e --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/scope/modules.rs @@ -0,0 +1,54 @@ +use nu_engine::{command_prelude::*, scope::ScopeData}; + +#[derive(Clone)] +pub struct ScopeModules; + +impl Command for ScopeModules { + fn name(&self) -> &str { + "scope modules" + } + + fn signature(&self) -> Signature { + Signature::build("scope modules") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .allow_variants_without_examples(true) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Output info on the modules in the current scope." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let mut scope_data = ScopeData::new(engine_state, stack); + scope_data.populate_modules(); + Ok(Value::list(scope_data.collect_modules(head), head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Show the modules in the current scope", + example: "scope modules", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ScopeModules {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/scope/variables.rs b/nushell/crates/nu-cmd-lang/src/core_commands/scope/variables.rs new file mode 100644 index 0000000..c07eb76 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/scope/variables.rs @@ -0,0 +1,54 @@ +use nu_engine::{command_prelude::*, scope::ScopeData}; + +#[derive(Clone)] +pub struct ScopeVariables; + +impl Command for ScopeVariables { + fn name(&self) -> &str { + "scope variables" + } + + fn signature(&self) -> Signature { + Signature::build("scope variables") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .allow_variants_without_examples(true) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Output info on the variables in the current scope." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let mut scope_data = ScopeData::new(engine_state, stack); + scope_data.populate_vars(); + Ok(Value::list(scope_data.collect_vars(head), head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Show the variables in the current scope", + example: "scope variables", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ScopeVariables {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/try_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/try_.rs new file mode 100644 index 0000000..e0570cb --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/try_.rs @@ -0,0 +1,89 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct Try; + +impl Command for Try { + fn name(&self) -> &str { + "try" + } + + fn description(&self) -> &str { + "Try to run a block, if it fails optionally run a catch closure." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("try") + .input_output_types(vec![(Type::Any, Type::Any)]) + .required("try_block", SyntaxShape::Block, "Block to run.") + .optional( + "catch_closure", + SyntaxShape::Keyword( + b"catch".to_vec(), + Box::new(SyntaxShape::OneOf(vec![ + SyntaxShape::Closure(None), + SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), + ])), + ), + "Closure to run if try block fails.", + ) + .category(Category::Core) + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'try' command: this code path should never be reached in IR mode" + ); + unreachable!(); + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Try to run a division by zero", + example: "try { 1 / 0 }", + result: None, + }, + Example { + description: "Try to run a division by zero and return a string instead", + example: "try { 1 / 0 } catch { 'divided by zero' }", + result: Some(Value::test_string("divided by zero")), + }, + Example { + description: "Try to run a division by zero and report the message", + example: "try { 1 / 0 } catch { |err| $err.msg }", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Try {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/use_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/use_.rs new file mode 100644 index 0000000..3968e28 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/use_.rs @@ -0,0 +1,213 @@ +use nu_engine::{ + command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block, redirect_env, +}; +use nu_protocol::{ + ast::{Expr, Expression}, + engine::CommandType, +}; + +#[derive(Clone)] +pub struct Use; + +impl Command for Use { + fn name(&self) -> &str { + "use" + } + + fn description(&self) -> &str { + "Use definitions from a module, making them available in your shell." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("use") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required( + "module", + SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]), + "Module or module file (`null` for no-op).", + ) + .rest( + "members", + SyntaxShape::Any, + "Which members of the module to import.", + ) + .category(Category::Core) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["module", "import", "include", "scope"] + } + + fn extra_description(&self) -> &str { + r#"See `help std` for the standard library module. +See `help modules` to list all available modules. + +This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + engine_state: &EngineState, + caller_stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + if call.get_parser_info(caller_stack, "noop").is_some() { + return Ok(PipelineData::empty()); + } + let Some(Expression { + expr: Expr::ImportPattern(import_pattern), + .. + }) = call.get_parser_info(caller_stack, "import_pattern") + else { + return Err(ShellError::GenericError { + error: "Unexpected import".into(), + msg: "import pattern not supported".into(), + span: Some(call.head), + help: None, + inner: vec![], + }); + }; + + // Necessary so that we can modify the stack. + let import_pattern = import_pattern.clone(); + + if let Some(module_id) = import_pattern.head.id { + // Add constants + for var_id in &import_pattern.constants { + let var = engine_state.get_var(*var_id); + + if let Some(constval) = &var.const_val { + caller_stack.add_var(*var_id, constval.clone()); + } else { + return Err(ShellError::NushellFailedSpanned { + msg: "Missing Constant".to_string(), + label: "constant not added by the parser".to_string(), + span: var.declaration_span, + }); + } + } + + // Evaluate the export-env block if there is one + let module = engine_state.get_module(module_id); + + if let Some(block_id) = module.env_block { + let block = engine_state.get_block(block_id); + + // See if the module is a file + let module_arg_str = String::from_utf8_lossy( + engine_state.get_span_contents(import_pattern.head.span), + ); + + let maybe_file_path_or_dir = find_in_dirs_env( + &module_arg_str, + engine_state, + caller_stack, + get_dirs_var_from_call(caller_stack, call), + )?; + // module_arg_str maybe a directory, in this case + // find_in_dirs_env returns a directory. + let maybe_parent = maybe_file_path_or_dir.as_ref().and_then(|path| { + if path.is_dir() { + Some(path.to_path_buf()) + } else { + path.parent().map(|p| p.to_path_buf()) + } + }); + + let mut callee_stack = caller_stack + .gather_captures(engine_state, &block.captures) + .reset_pipes(); + + // If so, set the currently evaluated directory (file-relative PWD) + if let Some(parent) = maybe_parent { + let file_pwd = Value::string(parent.to_string_lossy(), call.head); + callee_stack.add_env_var("FILE_PWD".to_string(), file_pwd); + } + + if let Some(path) = maybe_file_path_or_dir { + let module_file_path = if path.is_dir() { + // the existence of `mod.nu` is verified in parsing time + // so it's safe to use it here. + Value::string(path.join("mod.nu").to_string_lossy(), call.head) + } else { + Value::string(path.to_string_lossy(), call.head) + }; + callee_stack.add_env_var("CURRENT_FILE".to_string(), module_file_path); + } + + let eval_block = get_eval_block(engine_state); + + // Run the block (discard the result) + let _ = eval_block(engine_state, &mut callee_stack, block, input)?; + + // Merge the block's environment to the current stack + redirect_env(engine_state, caller_stack, &callee_stack); + } + } else { + return Err(ShellError::GenericError { + error: format!( + "Could not import from '{}'", + String::from_utf8_lossy(&import_pattern.head.name) + ), + msg: "module does not exist".to_string(), + span: Some(import_pattern.head.span), + help: None, + inner: vec![], + }); + } + + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Define a custom command in a module and call it", + example: r#"module spam { export def foo [] { "foo" } }; use spam foo; foo"#, + result: Some(Value::test_string("foo")), + }, + Example { + description: "Define a custom command that participates in the environment in a module and call it", + example: r#"module foo { export def --env bar [] { $env.FOO_BAR = "BAZ" } }; use foo bar; bar; $env.FOO_BAR"#, + result: Some(Value::test_string("BAZ")), + }, + Example { + description: "Use a plain module name to import its definitions qualified by the module name", + example: r#"module spam { export def foo [] { "foo" }; export def bar [] { "bar" } }; use spam; (spam foo) + (spam bar)"#, + result: Some(Value::test_string("foobar")), + }, + Example { + description: "Specify * to use all definitions in a module", + example: r#"module spam { export def foo [] { "foo" }; export def bar [] { "bar" } }; use spam *; (foo) + (bar)"#, + result: Some(Value::test_string("foobar")), + }, + Example { + description: "To use commands with spaces, like subcommands, surround them with quotes", + example: r#"module spam { export def 'foo bar' [] { "baz" } }; use spam 'foo bar'; foo bar"#, + result: Some(Value::test_string("baz")), + }, + Example { + description: "To use multiple definitions from a module, wrap them in a list", + example: r#"module spam { export def foo [] { "foo" }; export def 'foo bar' [] { "baz" } }; use spam ['foo', 'foo bar']; (foo) + (foo bar)"#, + result: Some(Value::test_string("foobaz")), + }, + ] + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Use; + use crate::test_examples; + test_examples(Use {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/version.rs b/nushell/crates/nu-cmd-lang/src/core_commands/version.rs new file mode 100644 index 0000000..eee2d34 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/version.rs @@ -0,0 +1,222 @@ +use std::{borrow::Cow, sync::OnceLock}; + +use itertools::Itertools; +use nu_engine::command_prelude::*; +use nu_protocol::engine::StateWorkingSet; +use shadow_rs::shadow; + +shadow!(build); + +/// Static container for the cargo features used by the `version` command. +/// +/// This `OnceLock` holds the features from `nu`. +/// When you build `nu_cmd_lang`, Cargo doesn't pass along the same features that `nu` itself uses. +/// By setting this static before calling `version`, you make it show `nu`'s features instead +/// of `nu_cmd_lang`'s. +/// +/// Embedders can set this to any feature list they need, but in most cases you'll probably want to +/// pass the cargo features of your host binary. +/// +/// # How to get cargo features in your build script +/// +/// In your binary's build script: +/// ```rust,ignore +/// // Re-export CARGO_CFG_FEATURE to the main binary. +/// // It holds all the features that cargo sets for your binary as a comma-separated list. +/// println!( +/// "cargo:rustc-env=NU_FEATURES={}", +/// std::env::var("CARGO_CFG_FEATURE").expect("set by cargo") +/// ); +/// ``` +/// +/// Then, before you call `version`: +/// ```rust,ignore +/// // This uses static strings, but since we're using `Cow`, you can also pass owned strings. +/// let features = env!("NU_FEATURES") +/// .split(',') +/// .map(Cow::Borrowed) +/// .collect(); +/// +/// nu_cmd_lang::VERSION_NU_FEATURES +/// .set(features) +/// .expect("couldn't set VERSION_NU_FEATURES"); +/// ``` +pub static VERSION_NU_FEATURES: OnceLock>> = OnceLock::new(); + +#[derive(Clone)] +pub struct Version; + +impl Command for Version { + fn name(&self) -> &str { + "version" + } + + fn signature(&self) -> Signature { + Signature::build("version") + .input_output_types(vec![(Type::Nothing, Type::record())]) + .allow_variants_without_examples(true) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Display Nu version, and its build configuration." + } + + fn is_const(&self) -> bool { + true + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + version(engine_state, call.head) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + version(working_set.permanent(), call.head) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Display Nu version", + example: "version", + result: None, + }] + } +} + +fn push_non_empty(record: &mut Record, name: &str, value: &str, span: Span) { + if !value.is_empty() { + record.push(name, Value::string(value, span)) + } +} + +pub fn version(engine_state: &EngineState, span: Span) -> Result { + // Pre-allocate the arrays in the worst case (17 items): + // - version + // - major + // - minor + // - patch + // - pre + // - branch + // - commit_hash + // - build_os + // - build_target + // - rust_version + // - rust_channel + // - cargo_version + // - build_time + // - build_rust_channel + // - allocator + // - features + // - installed_plugins + let mut record = Record::with_capacity(17); + + record.push("version", Value::string(env!("CARGO_PKG_VERSION"), span)); + + push_version_numbers(&mut record, span); + + push_non_empty(&mut record, "pre", build::PKG_VERSION_PRE, span); + + record.push("branch", Value::string(build::BRANCH, span)); + + if let Some(commit_hash) = option_env!("NU_COMMIT_HASH") { + record.push("commit_hash", Value::string(commit_hash, span)); + } + + push_non_empty(&mut record, "build_os", build::BUILD_OS, span); + push_non_empty(&mut record, "build_target", build::BUILD_TARGET, span); + push_non_empty(&mut record, "rust_version", build::RUST_VERSION, span); + push_non_empty(&mut record, "rust_channel", build::RUST_CHANNEL, span); + push_non_empty(&mut record, "cargo_version", build::CARGO_VERSION, span); + push_non_empty(&mut record, "build_time", build::BUILD_TIME, span); + push_non_empty( + &mut record, + "build_rust_channel", + build::BUILD_RUST_CHANNEL, + span, + ); + + record.push("allocator", Value::string(global_allocator(), span)); + + record.push( + "features", + Value::string( + VERSION_NU_FEATURES + .get() + .as_ref() + .map(|v| v.as_slice()) + .unwrap_or_default() + .iter() + .filter(|f| !f.starts_with("dep:")) + .join(", "), + span, + ), + ); + + #[cfg(not(feature = "plugin"))] + let _ = engine_state; + + #[cfg(feature = "plugin")] + { + // Get a list of plugin names and versions if present + let installed_plugins = engine_state + .plugins() + .iter() + .map(|x| { + let name = x.identity().name(); + if let Some(version) = x.metadata().and_then(|m| m.version) { + format!("{name} {version}") + } else { + name.into() + } + }) + .collect::>(); + + record.push( + "installed_plugins", + Value::string(installed_plugins.join(", "), span), + ); + } + + Ok(Value::record(record, span).into_pipeline_data()) +} + +/// Add version numbers as integers to the given record +fn push_version_numbers(record: &mut Record, head: Span) { + static VERSION_NUMBERS: OnceLock<(u8, u8, u8)> = OnceLock::new(); + + let &(major, minor, patch) = VERSION_NUMBERS.get_or_init(|| { + ( + build::PKG_VERSION_MAJOR.parse().expect("Always set"), + build::PKG_VERSION_MINOR.parse().expect("Always set"), + build::PKG_VERSION_PATCH.parse().expect("Always set"), + ) + }); + record.push("major", Value::int(major.into(), head)); + record.push("minor", Value::int(minor.into(), head)); + record.push("patch", Value::int(patch.into(), head)); +} + +fn global_allocator() -> &'static str { + "standard" +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Version; + use crate::test_examples; + test_examples(Version) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/core_commands/while_.rs b/nushell/crates/nu-cmd-lang/src/core_commands/while_.rs new file mode 100644 index 0000000..b64a0ec --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/core_commands/while_.rs @@ -0,0 +1,76 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct While; + +impl Command for While { + fn name(&self) -> &str { + "while" + } + + fn description(&self) -> &str { + "Conditionally run a block in a loop." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("while") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("cond", SyntaxShape::MathExpression, "Condition to check.") + .required( + "block", + SyntaxShape::Block, + "Block to loop if check succeeds.", + ) + .category(Category::Core) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["loop"] + } + + fn extra_description(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + eprintln!( + "Tried to execute 'run' for the 'while' command: this code path should never be reached in IR mode" + ); + unreachable!() + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Loop while a condition is true", + example: "mut x = 0; while $x < 10 { $x = $x + 1 }", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(While {}) + } +} diff --git a/nushell/crates/nu-cmd-lang/src/default_context.rs b/nushell/crates/nu-cmd-lang/src/default_context.rs new file mode 100644 index 0000000..e3b0d2d --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/default_context.rs @@ -0,0 +1,77 @@ +use crate::*; +use nu_protocol::engine::{EngineState, StateWorkingSet}; + +pub fn create_default_context() -> EngineState { + let mut engine_state = EngineState::new(); + + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + + macro_rules! bind_command { + ( $( $command:expr ),* $(,)? ) => { + $( working_set.add_decl(Box::new($command)); )* + }; + } + + // Core + bind_command! { + Alias, + AttrCategory, + AttrDeprecated, + AttrExample, + AttrSearchTerms, + Break, + Collect, + Const, + Continue, + Def, + Describe, + Do, + Echo, + ErrorMake, + ExportAlias, + ExportCommand, + ExportConst, + ExportDef, + ExportExtern, + ExportUse, + ExportModule, + Extern, + For, + Hide, + HideEnv, + If, + Ignore, + Overlay, + OverlayUse, + OverlayList, + OverlayNew, + OverlayHide, + Let, + Loop, + Match, + Module, + Mut, + Return, + Scope, + ScopeAliases, + ScopeCommands, + ScopeEngineStats, + ScopeExterns, + ScopeModules, + ScopeVariables, + Try, + Use, + Version, + While, + }; + + working_set.render() + }; + + if let Err(err) = engine_state.merge_delta(delta) { + eprintln!("Error creating default context: {err:?}"); + } + + engine_state +} diff --git a/nushell/crates/nu-cmd-lang/src/example_support.rs b/nushell/crates/nu-cmd-lang/src/example_support.rs new file mode 100644 index 0000000..d78e606 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/example_support.rs @@ -0,0 +1,327 @@ +use itertools::Itertools; +use nu_engine::{command_prelude::*, compile}; +use nu_protocol::{ + Range, ast::Block, debugger::WithoutDebug, engine::StateWorkingSet, report_shell_error, +}; +use std::{ + sync::Arc, + {collections::HashSet, ops::Bound}, +}; + +pub fn check_example_input_and_output_types_match_command_signature( + example: &Example, + cwd: &std::path::Path, + engine_state: &mut Box, + signature_input_output_types: &[(Type, Type)], + signature_operates_on_cell_paths: bool, +) -> HashSet<(Type, Type)> { + let mut witnessed_type_transformations = HashSet::<(Type, Type)>::new(); + + // Skip tests that don't have results to compare to + if let Some(example_output) = example.result.as_ref() { + if let Some(example_input) = + eval_pipeline_without_terminal_expression(example.example, cwd, engine_state) + { + let example_matches_signature = + signature_input_output_types + .iter() + .any(|(sig_in_type, sig_out_type)| { + example_input.is_subtype_of(sig_in_type) + && example_output.is_subtype_of(sig_out_type) + && { + witnessed_type_transformations + .insert((sig_in_type.clone(), sig_out_type.clone())); + true + } + }); + + let example_input_type = example_input.get_type(); + let example_output_type = example_output.get_type(); + + // The example type checks as a cell path operation if both: + // 1. The command is declared to operate on cell paths. + // 2. The example_input_type is list or record or table, and the example + // output shape is the same as the input shape. + let example_matches_signature_via_cell_path_operation = signature_operates_on_cell_paths + && example_input_type.accepts_cell_paths() + // TODO: This is too permissive; it should make use of the signature.input_output_types at least. + && example_output_type.to_shape() == example_input_type.to_shape(); + + if !(example_matches_signature || example_matches_signature_via_cell_path_operation) { + panic!( + "The example `{}` demonstrates a transformation of type {:?} -> {:?}. \ + However, this does not match the declared signature: {:?}.{} \ + For this command `operates_on_cell_paths()` is {}.", + example.example, + example_input_type, + example_output_type, + signature_input_output_types, + if signature_input_output_types.is_empty() { + " (Did you forget to declare the input and output types for the command?)" + } else { + "" + }, + signature_operates_on_cell_paths + ); + }; + }; + } + witnessed_type_transformations +} + +pub fn eval_pipeline_without_terminal_expression( + src: &str, + cwd: &std::path::Path, + engine_state: &mut Box, +) -> Option { + let (mut block, mut working_set) = parse(src, engine_state); + if block.pipelines.len() == 1 { + let n_expressions = block.pipelines[0].elements.len(); + // Modify the block to remove the last element and recompile it + { + let mut_block = Arc::make_mut(&mut block); + mut_block.pipelines[0].elements.truncate(n_expressions - 1); + mut_block.ir_block = Some(compile(&working_set, mut_block).expect( + "failed to compile block modified by eval_pipeline_without_terminal_expression", + )); + } + working_set.add_block(block.clone()); + engine_state + .merge_delta(working_set.render()) + .expect("failed to merge delta"); + + if !block.pipelines[0].elements.is_empty() { + let empty_input = PipelineData::empty(); + Some(eval_block(block, empty_input, cwd, engine_state)) + } else { + Some(Value::nothing(Span::test_data())) + } + } else { + // E.g. multiple semicolon-separated statements + None + } +} + +pub fn parse<'engine>( + contents: &str, + engine_state: &'engine EngineState, +) -> (Arc, StateWorkingSet<'engine>) { + let mut working_set = StateWorkingSet::new(engine_state); + let output = nu_parser::parse(&mut working_set, None, contents.as_bytes(), false); + + if let Some(err) = working_set.parse_errors.first() { + panic!("test parse error in `{contents}`: {err:?}"); + } + + if let Some(err) = working_set.compile_errors.first() { + panic!("test compile error in `{contents}`: {err:?}"); + } + + (output, working_set) +} + +pub fn eval_block( + block: Arc, + input: PipelineData, + cwd: &std::path::Path, + engine_state: &EngineState, +) -> Value { + let mut stack = Stack::new().collect_value(); + + stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy())); + + nu_engine::eval_block::(engine_state, &mut stack, &block, input) + .and_then(|data| data.into_value(Span::test_data())) + .unwrap_or_else(|err| { + report_shell_error(engine_state, &err); + panic!("test eval error in `{}`: {:?}", "TODO", err) + }) +} + +pub fn check_example_evaluates_to_expected_output( + cmd_name: &str, + example: &Example, + cwd: &std::path::Path, + engine_state: &mut Box, +) { + let mut stack = Stack::new().collect_value(); + + // Set up PWD + stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy())); + + engine_state + .merge_env(&mut stack) + .expect("Error merging environment"); + + let empty_input = PipelineData::empty(); + let result = eval(example.example, empty_input, cwd, engine_state); + + // Note. Value implements PartialEq for Bool, Int, Float, String and Block + // If the command you are testing requires to compare another case, then + // you need to define its equality in the Value struct + if let Some(expected) = example.result.as_ref() { + let expected = DebuggableValue(expected); + let result = DebuggableValue(&result); + assert_eq!( + result, expected, + "Error: The result of example '{}' for the command '{}' differs from the expected value.\n\nExpected: {:?}\nActual: {:?}\n", + example.description, cmd_name, expected, result, + ); + } +} + +pub fn check_all_signature_input_output_types_entries_have_examples( + signature: Signature, + witnessed_type_transformations: HashSet<(Type, Type)>, +) { + let declared_type_transformations = HashSet::from_iter(signature.input_output_types); + assert!( + witnessed_type_transformations.is_subset(&declared_type_transformations), + "This should not be possible (bug in test): the type transformations \ + collected in the course of matching examples to the signature type map \ + contain type transformations not present in the signature type map." + ); + + if !signature.allow_variants_without_examples { + assert_eq!( + witnessed_type_transformations, + declared_type_transformations, + "There are entries in the signature type map which do not correspond to any example: \ + {:?}", + declared_type_transformations + .difference(&witnessed_type_transformations) + .map(|(s1, s2)| format!("{s1} -> {s2}")) + .join(", ") + ); + } +} + +fn eval( + contents: &str, + input: PipelineData, + cwd: &std::path::Path, + engine_state: &mut Box, +) -> Value { + let (block, working_set) = parse(contents, engine_state); + engine_state + .merge_delta(working_set.render()) + .expect("failed to merge delta"); + eval_block(block, input, cwd, engine_state) +} + +pub struct DebuggableValue<'a>(pub &'a Value); + +impl PartialEq for DebuggableValue<'_> { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl std::fmt::Debug for DebuggableValue<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Value::Bool { val, .. } => { + write!(f, "{:?}", val) + } + Value::Int { val, .. } => { + write!(f, "{:?}", val) + } + Value::Float { val, .. } => { + write!(f, "{:?}f", val) + } + Value::Filesize { val, .. } => { + write!(f, "Filesize({:?})", val) + } + Value::Duration { val, .. } => { + let duration = std::time::Duration::from_nanos(*val as u64); + write!(f, "Duration({:?})", duration) + } + Value::Date { val, .. } => { + write!(f, "Date({:?})", val) + } + Value::Range { val, .. } => match **val { + Range::IntRange(range) => match range.end() { + Bound::Included(end) => write!( + f, + "Range({:?}..{:?}, step: {:?})", + range.start(), + end, + range.step(), + ), + Bound::Excluded(end) => write!( + f, + "Range({:?}..<{:?}, step: {:?})", + range.start(), + end, + range.step(), + ), + Bound::Unbounded => { + write!(f, "Range({:?}.., step: {:?})", range.start(), range.step()) + } + }, + Range::FloatRange(range) => match range.end() { + Bound::Included(end) => write!( + f, + "Range({:?}..{:?}, step: {:?})", + range.start(), + end, + range.step(), + ), + Bound::Excluded(end) => write!( + f, + "Range({:?}..<{:?}, step: {:?})", + range.start(), + end, + range.step(), + ), + Bound::Unbounded => { + write!(f, "Range({:?}.., step: {:?})", range.start(), range.step()) + } + }, + }, + Value::String { val, .. } | Value::Glob { val, .. } => { + write!(f, "{:?}", val) + } + Value::Record { val, .. } => { + write!(f, "{{")?; + let mut first = true; + for (col, value) in (&**val).into_iter() { + if !first { + write!(f, ", ")?; + } + first = false; + write!(f, "{:?}: {:?}", col, DebuggableValue(value))?; + } + write!(f, "}}") + } + Value::List { vals, .. } => { + write!(f, "[")?; + for (i, value) in vals.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{:?}", DebuggableValue(value))?; + } + write!(f, "]") + } + Value::Closure { val, .. } => { + write!(f, "Closure({:?})", val) + } + Value::Nothing { .. } => { + write!(f, "Nothing") + } + Value::Error { error, .. } => { + write!(f, "Error({:?})", error) + } + Value::Binary { val, .. } => { + write!(f, "Binary({:?})", val) + } + Value::CellPath { val, .. } => { + write!(f, "CellPath({:?})", val.to_string()) + } + Value::Custom { val, .. } => { + write!(f, "CustomValue({:?})", val) + } + } + } +} diff --git a/nushell/crates/nu-cmd-lang/src/example_test.rs b/nushell/crates/nu-cmd-lang/src/example_test.rs new file mode 100644 index 0000000..d334d63 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/example_test.rs @@ -0,0 +1,99 @@ +#[cfg(test)] +use nu_protocol::engine::Command; + +#[cfg(test)] +pub fn test_examples(cmd: impl Command + 'static) { + test_examples::test_examples(cmd); +} + +#[cfg(test)] +mod test_examples { + use crate::example_support::{ + check_all_signature_input_output_types_entries_have_examples, + check_example_evaluates_to_expected_output, + check_example_input_and_output_types_match_command_signature, + }; + use crate::{ + Break, Collect, Def, Describe, Echo, ExportCommand, ExportDef, If, Let, Module, Mut, Use, + }; + use nu_protocol::{ + Type, Value, + engine::{Command, EngineState, StateWorkingSet}, + }; + use std::collections::HashSet; + + pub fn test_examples(cmd: impl Command + 'static) { + let examples = cmd.examples(); + let signature = cmd.signature(); + let mut engine_state = make_engine_state(cmd.clone_box()); + + let cwd = std::env::current_dir().expect("Could not get current working directory."); + + let mut witnessed_type_transformations = HashSet::<(Type, Type)>::new(); + + for example in examples { + if example.result.is_none() { + continue; + } + witnessed_type_transformations.extend( + check_example_input_and_output_types_match_command_signature( + &example, + &cwd, + &mut make_engine_state(cmd.clone_box()), + &signature.input_output_types, + signature.operates_on_cell_paths(), + ), + ); + check_example_evaluates_to_expected_output( + cmd.name(), + &example, + cwd.as_path(), + &mut engine_state, + ); + } + + check_all_signature_input_output_types_entries_have_examples( + signature, + witnessed_type_transformations, + ); + } + + fn make_engine_state(cmd: Box) -> Box { + let mut engine_state = Box::new(EngineState::new()); + let cwd = std::env::current_dir() + .expect("Could not get current working directory.") + .to_string_lossy() + .to_string(); + engine_state.add_env_var("PWD".to_string(), Value::test_string(cwd)); + + let delta = { + // Base functions that are needed for testing + // Try to keep this working set small to keep tests running as fast as possible + let mut working_set = StateWorkingSet::new(&engine_state); + working_set.add_decl(Box::new(Break)); + working_set.add_decl(Box::new(Collect)); + working_set.add_decl(Box::new(Def)); + working_set.add_decl(Box::new(Describe)); + working_set.add_decl(Box::new(Echo)); + working_set.add_decl(Box::new(ExportCommand)); + working_set.add_decl(Box::new(ExportDef)); + working_set.add_decl(Box::new(If)); + working_set.add_decl(Box::new(Let)); + working_set.add_decl(Box::new(Module)); + working_set.add_decl(Box::new(Mut)); + working_set.add_decl(Box::new(Use)); + + // Adding the command that is being tested to the working set + working_set.add_decl(cmd); + + working_set.render() + }; + + engine_state.add_env_var("PWD".to_string(), Value::test_string(".")); + + engine_state + .merge_delta(delta) + .expect("Error merging delta"); + engine_state + } +} diff --git a/nushell/crates/nu-cmd-lang/src/lib.rs b/nushell/crates/nu-cmd-lang/src/lib.rs new file mode 100644 index 0000000..3d6f506 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/lib.rs @@ -0,0 +1,14 @@ +#![cfg_attr(not(feature = "os"), allow(unused))] +#![doc = include_str!("../README.md")] +mod core_commands; +mod default_context; +pub mod example_support; +mod example_test; +#[cfg(test)] +mod parse_const_test; + +pub use core_commands::*; +pub use default_context::*; +pub use example_support::*; +#[cfg(test)] +pub use example_test::test_examples; diff --git a/nushell/crates/nu-cmd-lang/src/parse_const_test.rs b/nushell/crates/nu-cmd-lang/src/parse_const_test.rs new file mode 100644 index 0000000..e0b90a9 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/src/parse_const_test.rs @@ -0,0 +1,19 @@ +use nu_protocol::{Span, engine::StateWorkingSet}; +use quickcheck_macros::quickcheck; + +#[quickcheck] +fn quickcheck_parse(data: String) -> bool { + let (tokens, err) = nu_parser::lex(data.as_bytes(), 0, b"", b"", true); + + if err.is_none() { + let context = crate::create_default_context(); + { + let mut working_set = StateWorkingSet::new(&context); + let _ = working_set.add_file("quickcheck".into(), data.as_bytes()); + + let _ = + nu_parser::parse_block(&mut working_set, &tokens, Span::new(0, 0), false, false); + } + } + true +} diff --git a/nushell/crates/nu-cmd-lang/tests/commands/attr/deprecated.rs b/nushell/crates/nu-cmd-lang/tests/commands/attr/deprecated.rs new file mode 100644 index 0000000..7ddda3a --- /dev/null +++ b/nushell/crates/nu-cmd-lang/tests/commands/attr/deprecated.rs @@ -0,0 +1,114 @@ +use miette::{Diagnostic, LabeledSpan}; +use nu_cmd_lang::{Alias, Def}; +use nu_parser::parse; +use nu_protocol::engine::{EngineState, StateWorkingSet}; + +use nu_cmd_lang::AttrDeprecated; + +#[test] +pub fn test_deprecated_attribute() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_decl(Box::new(Def)); + working_set.add_decl(Box::new(Alias)); + working_set.add_decl(Box::new(AttrDeprecated)); + + // test deprecation with no message + let source = br#" + @deprecated + def foo [] {} + "#; + let _ = parse(&mut working_set, None, source, false); + + // there should be no warning until the command is called + assert!(working_set.parse_errors.is_empty()); + assert!(working_set.parse_warnings.is_empty()); + + let source = b"foo"; + let _ = parse(&mut working_set, None, source, false); + + // command called, there should be a deprecation warning + assert!(working_set.parse_errors.is_empty()); + assert!(!working_set.parse_warnings.is_empty()); + let labels: Vec = working_set.parse_warnings[0].labels().unwrap().collect(); + let label = labels.first().unwrap().label().unwrap(); + assert!(label.contains("foo is deprecated")); + working_set.parse_warnings.clear(); + + // test deprecation with message + let source = br#" + @deprecated "Use new-command instead" + def old-command [] {} + + old-command + "#; + let _ = parse(&mut working_set, None, source, false); + + assert!(working_set.parse_errors.is_empty()); + assert!(!working_set.parse_warnings.is_empty()); + + let help = &working_set.parse_warnings[0].help().unwrap().to_string(); + assert!(help.contains("Use new-command instead")); +} + +#[test] +pub fn test_deprecated_attribute_flag() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_decl(Box::new(Def)); + working_set.add_decl(Box::new(Alias)); + working_set.add_decl(Box::new(AttrDeprecated)); + + let source = br#" + @deprecated "Use foo instead of bar" --flag bar + @deprecated "Use foo instead of baz" --flag baz + def old-command [--foo, --bar, --baz] {} + old-command --foo + old-command --bar + old-command --baz + old-command --foo --bar --baz + "#; + let _ = parse(&mut working_set, None, source, false); + + assert!(working_set.parse_errors.is_empty()); + assert!(!working_set.parse_warnings.is_empty()); + + let help = &working_set.parse_warnings[0].help().unwrap().to_string(); + assert!(help.contains("Use foo instead of bar")); + + let help = &working_set.parse_warnings[1].help().unwrap().to_string(); + assert!(help.contains("Use foo instead of baz")); + + let help = &working_set.parse_warnings[2].help().unwrap().to_string(); + assert!(help.contains("Use foo instead of bar")); + + let help = &working_set.parse_warnings[3].help().unwrap().to_string(); + assert!(help.contains("Use foo instead of baz")); +} + +#[test] +pub fn test_deprecated_attribute_since_remove() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_decl(Box::new(Def)); + working_set.add_decl(Box::new(Alias)); + working_set.add_decl(Box::new(AttrDeprecated)); + + let source = br#" + @deprecated --since 0.10000.0 --remove 1.0 + def old-command [] {} + old-command + "#; + let _ = parse(&mut working_set, None, source, false); + + assert!(working_set.parse_errors.is_empty()); + assert!(!working_set.parse_warnings.is_empty()); + + let labels: Vec = working_set.parse_warnings[0].labels().unwrap().collect(); + let label = labels.first().unwrap().label().unwrap(); + assert!(label.contains("0.10000.0")); + assert!(label.contains("1.0")); +} diff --git a/nushell/crates/nu-cmd-lang/tests/commands/attr/mod.rs b/nushell/crates/nu-cmd-lang/tests/commands/attr/mod.rs new file mode 100644 index 0000000..312dcd9 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/tests/commands/attr/mod.rs @@ -0,0 +1 @@ +mod deprecated; diff --git a/nushell/crates/nu-cmd-lang/tests/commands/mod.rs b/nushell/crates/nu-cmd-lang/tests/commands/mod.rs new file mode 100644 index 0000000..ddd623b --- /dev/null +++ b/nushell/crates/nu-cmd-lang/tests/commands/mod.rs @@ -0,0 +1 @@ +mod attr; diff --git a/nushell/crates/nu-cmd-lang/tests/main.rs b/nushell/crates/nu-cmd-lang/tests/main.rs new file mode 100644 index 0000000..f3d4468 --- /dev/null +++ b/nushell/crates/nu-cmd-lang/tests/main.rs @@ -0,0 +1 @@ +mod commands; diff --git a/nushell/crates/nu-cmd-plugin/Cargo.toml b/nushell/crates/nu-cmd-plugin/Cargo.toml new file mode 100644 index 0000000..54ef9f1 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Commands for managing Nushell plugins." +edition = "2024" +license = "MIT" +name = "nu-cmd-plugin" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin" +version = "0.105.2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lints] +workspace = true + +[dependencies] +nu-engine = { path = "../nu-engine", version = "0.105.2" } +nu-path = { path = "../nu-path", version = "0.105.2" } +nu-protocol = { path = "../nu-protocol", version = "0.105.2", features = ["plugin"] } +nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.105.2" } + +itertools = { workspace = true } + +[dev-dependencies] diff --git a/nushell/crates/nu-cmd-plugin/LICENSE b/nushell/crates/nu-cmd-plugin/LICENSE new file mode 100644 index 0000000..ae174e8 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nushell/crates/nu-cmd-plugin/README.md b/nushell/crates/nu-cmd-plugin/README.md new file mode 100644 index 0000000..0c62ad1 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/README.md @@ -0,0 +1,3 @@ +# nu-cmd-plugin + +This crate implements Nushell commands related to plugin management. diff --git a/nushell/crates/nu-cmd-plugin/src/commands/mod.rs b/nushell/crates/nu-cmd-plugin/src/commands/mod.rs new file mode 100644 index 0000000..17ff32f --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/commands/mod.rs @@ -0,0 +1,3 @@ +mod plugin; + +pub use plugin::*; diff --git a/nushell/crates/nu-cmd-plugin/src/commands/plugin/add.rs b/nushell/crates/nu-cmd-plugin/src/commands/plugin/add.rs new file mode 100644 index 0000000..1e9b441 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/commands/plugin/add.rs @@ -0,0 +1,142 @@ +use crate::util::{get_plugin_dirs, modify_plugin_file}; +use nu_engine::command_prelude::*; +use nu_plugin_engine::{GetPlugin, PersistentPlugin}; +use nu_protocol::{ + PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin, shell_error::io::IoError, +}; +use std::{path::PathBuf, sync::Arc}; + +#[derive(Clone)] +pub struct PluginAdd; + +impl Command for PluginAdd { + fn name(&self) -> &str { + "plugin add" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_type(Type::Nothing, Type::Nothing) + // This matches the option to `nu` + .named( + "plugin-config", + SyntaxShape::Filepath, + "Use a plugin registry file other than the one set in `$nu.plugin-path`", + None, + ) + .named( + "shell", + SyntaxShape::Filepath, + "Use an additional shell program (cmd, sh, python, etc.) to run the plugin", + Some('s'), + ) + .required( + "filename", + SyntaxShape::String, + "Path to the executable for the plugin.", + ) + .category(Category::Plugin) + } + + fn description(&self) -> &str { + "Add a plugin to the plugin registry file." + } + + fn extra_description(&self) -> &str { + r#" +This does not load the plugin commands into the scope - see `plugin use` for +that. + +Instead, it runs the plugin to get its command signatures, and then edits the +plugin registry file (by default, `$nu.plugin-path`). The changes will be +apparent the next time `nu` is next launched with that plugin registry file. +"# + .trim() + } + + fn search_terms(&self) -> Vec<&str> { + vec!["load", "register", "signature"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "plugin add nu_plugin_inc", + description: "Run the `nu_plugin_inc` plugin from the current directory or $env.NU_PLUGIN_DIRS and install its signatures.", + result: None, + }, + Example { + example: "plugin add --plugin-config polars.msgpackz nu_plugin_polars", + description: "Run the `nu_plugin_polars` plugin from the current directory or $env.NU_PLUGIN_DIRS, and install its signatures to the \"polars.msgpackz\" plugin registry file.", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let filename: Spanned = call.req(engine_state, stack, 0)?; + let shell: Option> = call.get_flag(engine_state, stack, "shell")?; + let cwd = engine_state.cwd(Some(stack))?; + + // Check the current directory, or fall back to NU_PLUGIN_DIRS + let filename_expanded = nu_path::locate_in_dirs(&filename.item, &cwd, || { + get_plugin_dirs(engine_state, stack) + }) + .map_err(|err| { + IoError::new( + err.not_found_as(NotFound::File), + filename.span, + PathBuf::from(filename.item), + ) + })?; + + let shell_expanded = shell + .as_ref() + .map(|s| { + nu_path::canonicalize_with(&s.item, &cwd) + .map_err(|err| IoError::new(err, s.span, None)) + }) + .transpose()?; + + // Parse the plugin filename so it can be used to spawn the plugin + let identity = PluginIdentity::new(filename_expanded, shell_expanded).map_err(|_| { + ShellError::GenericError { + error: "Plugin filename is invalid".into(), + msg: "plugin executable files must start with `nu_plugin_`".into(), + span: Some(filename.span), + help: None, + inner: vec![], + } + })?; + + let custom_path = call.get_flag(engine_state, stack, "plugin-config")?; + + // Start the plugin manually, to get the freshest signatures and to not affect engine + // state. Provide a GC config that will stop it ASAP + let plugin = Arc::new(PersistentPlugin::new( + identity, + PluginGcConfig { + enabled: true, + stop_after: 0, + }, + )); + let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?; + let metadata = interface.get_metadata()?; + let commands = interface.get_signature()?; + + modify_plugin_file(engine_state, stack, call.head, &custom_path, |contents| { + // Update the file with the received metadata and signatures + let item = PluginRegistryItem::new(plugin.identity(), metadata, commands); + contents.upsert_plugin(item); + Ok(()) + })?; + + Ok(Value::nothing(call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cmd-plugin/src/commands/plugin/list.rs b/nushell/crates/nu-cmd-plugin/src/commands/plugin/list.rs new file mode 100644 index 0000000..367a532 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/commands/plugin/list.rs @@ -0,0 +1,301 @@ +use itertools::{EitherOrBoth, Itertools}; +use nu_engine::command_prelude::*; +use nu_protocol::{IntoValue, PluginRegistryItemData}; + +use crate::util::read_plugin_file; + +#[derive(Clone)] +pub struct PluginList; + +impl Command for PluginList { + fn name(&self) -> &str { + "plugin list" + } + + fn signature(&self) -> Signature { + Signature::build("plugin list") + .input_output_type( + Type::Nothing, + Type::Table( + [ + ("name".into(), Type::String), + ("version".into(), Type::String), + ("status".into(), Type::String), + ("pid".into(), Type::Int), + ("filename".into(), Type::String), + ("shell".into(), Type::String), + ("commands".into(), Type::List(Type::String.into())), + ] + .into(), + ), + ) + .named( + "plugin-config", + SyntaxShape::Filepath, + "Use a plugin registry file other than the one set in `$nu.plugin-path`", + None, + ) + .switch( + "engine", + "Show info for plugins that are loaded into the engine only.", + Some('e'), + ) + .switch( + "registry", + "Show info for plugins from the registry file only.", + Some('r'), + ) + .category(Category::Plugin) + } + + fn description(&self) -> &str { + "List loaded and installed plugins." + } + + fn extra_description(&self) -> &str { + r#" +The `status` column will contain one of the following values: + +- `added`: The plugin is present in the plugin registry file, but not in + the engine. +- `loaded`: The plugin is present both in the plugin registry file and in + the engine, but is not running. +- `running`: The plugin is currently running, and the `pid` column should + contain its process ID. +- `modified`: The plugin state present in the plugin registry file is different + from the state in the engine. +- `removed`: The plugin is still loaded in the engine, but is not present in + the plugin registry file. +- `invalid`: The data in the plugin registry file couldn't be deserialized, + and the plugin most likely needs to be added again. + +`running` takes priority over any other status. Unless `--registry` is used +or the plugin has not been loaded yet, the values of `version`, `filename`, +`shell`, and `commands` reflect the values in the engine and not the ones in +the plugin registry file. + +See also: `plugin use` +"# + .trim() + } + + fn search_terms(&self) -> Vec<&str> { + vec!["scope"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "plugin list", + description: "List installed plugins.", + result: Some(Value::test_list(vec![Value::test_record(record! { + "name" => Value::test_string("inc"), + "version" => Value::test_string(env!("CARGO_PKG_VERSION")), + "status" => Value::test_string("running"), + "pid" => Value::test_int(106480), + "filename" => if cfg!(windows) { + Value::test_string(r"C:\nu\plugins\nu_plugin_inc.exe") + } else { + Value::test_string("/opt/nu/plugins/nu_plugin_inc") + }, + "shell" => Value::test_nothing(), + "commands" => Value::test_list(vec![Value::test_string("inc")]), + })])), + }, + Example { + example: "ps | where pid in (plugin list).pid", + description: "Get process information for running plugins.", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let custom_path = call.get_flag(engine_state, stack, "plugin-config")?; + let engine_mode = call.has_flag(engine_state, stack, "engine")?; + let registry_mode = call.has_flag(engine_state, stack, "registry")?; + + let plugins_info = match (engine_mode, registry_mode) { + // --engine and --registry together is equivalent to the default. + (false, false) | (true, true) => { + if engine_state.plugin_path.is_some() || custom_path.is_some() { + let plugins_in_engine = get_plugins_in_engine(engine_state); + let plugins_in_registry = + get_plugins_in_registry(engine_state, stack, call.head, &custom_path)?; + merge_plugin_info(plugins_in_engine, plugins_in_registry) + } else { + // Don't produce error when running nu --no-config-file + get_plugins_in_engine(engine_state) + } + } + (true, false) => get_plugins_in_engine(engine_state), + (false, true) => get_plugins_in_registry(engine_state, stack, call.head, &custom_path)?, + }; + + Ok(plugins_info.into_value(call.head).into_pipeline_data()) + } +} + +#[derive(Debug, Clone, IntoValue, PartialOrd, Ord, PartialEq, Eq)] +struct PluginInfo { + name: String, + version: Option, + status: PluginStatus, + pid: Option, + filename: String, + shell: Option, + commands: Vec, +} + +#[derive(Debug, Clone, Copy, IntoValue, PartialOrd, Ord, PartialEq, Eq)] +#[nu_value(rename_all = "snake_case")] +enum PluginStatus { + Added, + Loaded, + Running, + Modified, + Removed, + Invalid, +} + +fn get_plugins_in_engine(engine_state: &EngineState) -> Vec { + // Group plugin decls by plugin identity + let decls = engine_state.plugin_decls().into_group_map_by(|decl| { + decl.plugin_identity() + .expect("plugin decl should have identity") + }); + + // Build plugins list + engine_state + .plugins() + .iter() + .map(|plugin| { + // Find commands that belong to the plugin + let commands = decls + .get(plugin.identity()) + .into_iter() + .flat_map(|decls| decls.iter().map(|decl| decl.name().to_owned())) + .sorted() + .collect(); + + PluginInfo { + name: plugin.identity().name().into(), + version: plugin.metadata().and_then(|m| m.version), + status: if plugin.pid().is_some() { + PluginStatus::Running + } else { + PluginStatus::Loaded + }, + pid: plugin.pid(), + filename: plugin.identity().filename().to_string_lossy().into_owned(), + shell: plugin + .identity() + .shell() + .map(|path| path.to_string_lossy().into_owned()), + commands, + } + }) + .sorted() + .collect() +} + +fn get_plugins_in_registry( + engine_state: &EngineState, + stack: &mut Stack, + span: Span, + custom_path: &Option>, +) -> Result, ShellError> { + let plugin_file_contents = read_plugin_file(engine_state, stack, span, custom_path)?; + + let plugins_info = plugin_file_contents + .plugins + .into_iter() + .map(|plugin| { + let mut info = PluginInfo { + name: plugin.name, + version: None, + status: PluginStatus::Added, + pid: None, + filename: plugin.filename.to_string_lossy().into_owned(), + shell: plugin.shell.map(|path| path.to_string_lossy().into_owned()), + commands: vec![], + }; + + if let PluginRegistryItemData::Valid { metadata, commands } = plugin.data { + info.version = metadata.version; + info.commands = commands + .into_iter() + .map(|command| command.sig.name) + .sorted() + .collect(); + } else { + info.status = PluginStatus::Invalid; + } + info + }) + .sorted() + .collect(); + + Ok(plugins_info) +} + +/// If no options are provided, the command loads from both the plugin list in the engine and what's +/// in the registry file. We need to reconcile the two to set the proper states and make sure that +/// new plugins that were added to the plugin registry file show up. +fn merge_plugin_info( + from_engine: Vec, + from_registry: Vec, +) -> Vec { + from_engine + .into_iter() + .merge_join_by(from_registry, |info_a, info_b| { + info_a.name.cmp(&info_b.name) + }) + .map(|either_or_both| match either_or_both { + // Exists in the engine, but not in the registry file + EitherOrBoth::Left(info) => PluginInfo { + status: match info.status { + PluginStatus::Running => info.status, + // The plugin is not in the registry file, so it should be marked as `removed` + _ => PluginStatus::Removed, + }, + ..info + }, + // Exists in the registry file, but not in the engine + EitherOrBoth::Right(info) => info, + // Exists in both + EitherOrBoth::Both(info_engine, info_registry) => PluginInfo { + status: match (info_engine.status, info_registry.status) { + // Above all, `running` should be displayed if the plugin is running + (PluginStatus::Running, _) => PluginStatus::Running, + // `invalid` takes precedence over other states because the user probably wants + // to fix it + (_, PluginStatus::Invalid) => PluginStatus::Invalid, + // Display `modified` if the state in the registry is different somehow + _ if info_engine.is_modified(&info_registry) => PluginStatus::Modified, + // Otherwise, `loaded` (it's not running) + _ => PluginStatus::Loaded, + }, + ..info_engine + }, + }) + .sorted() + .collect() +} + +impl PluginInfo { + /// True if the plugin info shows some kind of change (other than status/pid) relative to the + /// other + fn is_modified(&self, other: &PluginInfo) -> bool { + self.name != other.name + || self.filename != other.filename + || self.shell != other.shell + || self.commands != other.commands + } +} diff --git a/nushell/crates/nu-cmd-plugin/src/commands/plugin/mod.rs b/nushell/crates/nu-cmd-plugin/src/commands/plugin/mod.rs new file mode 100644 index 0000000..ae4e5a7 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/commands/plugin/mod.rs @@ -0,0 +1,77 @@ +use nu_engine::{command_prelude::*, get_full_help}; + +mod add; +mod list; +mod rm; +mod stop; +mod use_; + +pub use add::PluginAdd; +pub use list::PluginList; +pub use rm::PluginRm; +pub use stop::PluginStop; +pub use use_::PluginUse; + +#[derive(Clone)] +pub struct PluginCommand; + +impl Command for PluginCommand { + fn name(&self) -> &str { + "plugin" + } + + fn signature(&self) -> Signature { + Signature::build("plugin") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .category(Category::Plugin) + } + + fn description(&self) -> &str { + "Commands for managing plugins." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "plugin add nu_plugin_inc", + description: "Run the `nu_plugin_inc` plugin from the current directory and install its signatures.", + result: None, + }, + Example { + example: "plugin use inc", + description: " +Load (or reload) the `inc` plugin from the plugin registry file and put its +commands in scope. The plugin must already be in the registry file at parse +time. +" + .trim(), + result: None, + }, + Example { + example: "plugin list", + description: "List installed plugins", + result: None, + }, + Example { + example: "plugin stop inc", + description: "Stop the plugin named `inc`.", + result: None, + }, + Example { + example: "plugin rm inc", + description: "Remove the installed signatures for the `inc` plugin.", + result: None, + }, + ] + } +} diff --git a/nushell/crates/nu-cmd-plugin/src/commands/plugin/rm.rs b/nushell/crates/nu-cmd-plugin/src/commands/plugin/rm.rs new file mode 100644 index 0000000..82b909a --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/commands/plugin/rm.rs @@ -0,0 +1,113 @@ +use nu_engine::command_prelude::*; + +use crate::util::{canonicalize_possible_filename_arg, modify_plugin_file}; + +#[derive(Clone)] +pub struct PluginRm; + +impl Command for PluginRm { + fn name(&self) -> &str { + "plugin rm" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_type(Type::Nothing, Type::Nothing) + // This matches the option to `nu` + .named( + "plugin-config", + SyntaxShape::Filepath, + "Use a plugin registry file other than the one set in `$nu.plugin-path`", + None, + ) + .switch( + "force", + "Don't cause an error if the plugin name wasn't found in the file", + Some('f'), + ) + .required( + "name", + SyntaxShape::String, + "The name, or filename, of the plugin to remove.", + ) + .category(Category::Plugin) + } + + fn description(&self) -> &str { + "Remove a plugin from the plugin registry file." + } + + fn extra_description(&self) -> &str { + r#" +This does not remove the plugin commands from the current scope or from `plugin +list` in the current shell. It instead removes the plugin from the plugin +registry file (by default, `$nu.plugin-path`). The changes will be apparent the +next time `nu` is launched with that plugin registry file. + +This can be useful for removing an invalid plugin signature, if it can't be +fixed with `plugin add`. +"# + .trim() + } + + fn search_terms(&self) -> Vec<&str> { + vec!["remove", "delete", "signature"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "plugin rm inc", + description: "Remove the installed signatures for the `inc` plugin.", + result: None, + }, + Example { + example: "plugin rm ~/.cargo/bin/nu_plugin_inc", + description: "Remove the installed signatures for the plugin with the filename `~/.cargo/bin/nu_plugin_inc`.", + result: None, + }, + Example { + example: "plugin rm --plugin-config polars.msgpackz polars", + description: "Remove the installed signatures for the `polars` plugin from the \"polars.msgpackz\" plugin registry file.", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let name: Spanned = call.req(engine_state, stack, 0)?; + let custom_path = call.get_flag(engine_state, stack, "plugin-config")?; + let force = call.has_flag(engine_state, stack, "force")?; + + let filename = canonicalize_possible_filename_arg(engine_state, stack, &name.item); + + modify_plugin_file(engine_state, stack, call.head, &custom_path, |contents| { + if let Some(index) = contents + .plugins + .iter() + .position(|p| p.name == name.item || p.filename == filename) + { + contents.plugins.remove(index); + Ok(()) + } else if force { + Ok(()) + } else { + Err(ShellError::GenericError { + error: format!("Failed to remove the `{}` plugin", name.item), + msg: "couldn't find a plugin with this name in the registry file".into(), + span: Some(name.span), + help: None, + inner: vec![], + }) + } + })?; + + Ok(Value::nothing(call.head).into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-cmd-plugin/src/commands/plugin/stop.rs b/nushell/crates/nu-cmd-plugin/src/commands/plugin/stop.rs new file mode 100644 index 0000000..3437294 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/commands/plugin/stop.rs @@ -0,0 +1,80 @@ +use nu_engine::command_prelude::*; + +use crate::util::canonicalize_possible_filename_arg; + +#[derive(Clone)] +pub struct PluginStop; + +impl Command for PluginStop { + fn name(&self) -> &str { + "plugin stop" + } + + fn signature(&self) -> Signature { + Signature::build("plugin stop") + .input_output_type(Type::Nothing, Type::Nothing) + .required( + "name", + SyntaxShape::String, + "The name, or filename, of the plugin to stop.", + ) + .category(Category::Plugin) + } + + fn description(&self) -> &str { + "Stop an installed plugin if it was running." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "plugin stop inc", + description: "Stop the plugin named `inc`.", + result: None, + }, + Example { + example: "plugin stop ~/.cargo/bin/nu_plugin_inc", + description: "Stop the plugin with the filename `~/.cargo/bin/nu_plugin_inc`.", + result: None, + }, + Example { + example: "plugin list | each { |p| plugin stop $p.name }", + description: "Stop all plugins.", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let name: Spanned = call.req(engine_state, stack, 0)?; + + let filename = canonicalize_possible_filename_arg(engine_state, stack, &name.item); + + let mut found = false; + for plugin in engine_state.plugins() { + let id = &plugin.identity(); + if id.name() == name.item || id.filename() == filename { + plugin.stop()?; + found = true; + } + } + + if found { + Ok(PipelineData::Empty) + } else { + Err(ShellError::GenericError { + error: format!("Failed to stop the `{}` plugin", name.item), + msg: "couldn't find a plugin with this name".into(), + span: Some(name.span), + help: Some("you may need to `plugin add` the plugin first".into()), + inner: vec![], + }) + } + } +} diff --git a/nushell/crates/nu-cmd-plugin/src/commands/plugin/use_.rs b/nushell/crates/nu-cmd-plugin/src/commands/plugin/use_.rs new file mode 100644 index 0000000..64fec14 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/commands/plugin/use_.rs @@ -0,0 +1,89 @@ +use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; + +#[derive(Clone)] +pub struct PluginUse; + +impl Command for PluginUse { + fn name(&self) -> &str { + "plugin use" + } + + fn description(&self) -> &str { + "Load a plugin from the plugin registry file into scope." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build(self.name()) + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .named( + "plugin-config", + SyntaxShape::Filepath, + "Use a plugin registry file other than the one set in `$nu.plugin-path`", + None, + ) + .required( + "name", + SyntaxShape::String, + "The name, or filename, of the plugin to load.", + ) + .category(Category::Plugin) + } + + fn extra_description(&self) -> &str { + r#" +This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html + +The plugin definition must be available in the plugin registry file at parse +time. Run `plugin add` first in the REPL to do this, or from a script consider +preparing a plugin registry file and passing `--plugin-config`, or using the +`--plugin` option to `nu` instead. + +If the plugin was already loaded, this will reload the latest definition from +the registry file into scope. + +Note that even if the plugin filename is specified, it will only be loaded if +it was already previously registered with `plugin add`. +"# + .trim() + } + + fn search_terms(&self) -> Vec<&str> { + vec!["add", "register", "scope"] + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Load the commands for the `query` plugin from $nu.plugin-path", + example: r#"plugin use query"#, + result: None, + }, + Example { + description: "Load the commands for the plugin with the filename `~/.cargo/bin/nu_plugin_query` from $nu.plugin-path", + example: r#"plugin use ~/.cargo/bin/nu_plugin_query"#, + result: None, + }, + Example { + description: "Load the commands for the `query` plugin from a custom plugin registry file", + example: r#"plugin use --plugin-config local-plugins.msgpackz query"#, + result: None, + }, + ] + } +} diff --git a/nushell/crates/nu-cmd-plugin/src/default_context.rs b/nushell/crates/nu-cmd-plugin/src/default_context.rs new file mode 100644 index 0000000..1ef6a90 --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/default_context.rs @@ -0,0 +1,31 @@ +use crate::*; +use nu_protocol::engine::{EngineState, StateWorkingSet}; + +pub fn add_plugin_command_context(mut engine_state: EngineState) -> EngineState { + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + + macro_rules! bind_command { + ( $( $command:expr ),* $(,)? ) => { + $( working_set.add_decl(Box::new($command)); )* + }; + } + + bind_command!( + PluginAdd, + PluginCommand, + PluginList, + PluginRm, + PluginStop, + PluginUse, + ); + + working_set.render() + }; + + if let Err(err) = engine_state.merge_delta(delta) { + eprintln!("Error creating default context: {err:?}"); + } + + engine_state +} diff --git a/nushell/crates/nu-cmd-plugin/src/lib.rs b/nushell/crates/nu-cmd-plugin/src/lib.rs new file mode 100644 index 0000000..d6fb5bb --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/lib.rs @@ -0,0 +1,8 @@ +//! Nushell commands for managing plugins. + +mod commands; +mod default_context; +mod util; + +pub use commands::*; +pub use default_context::*; diff --git a/nushell/crates/nu-cmd-plugin/src/util.rs b/nushell/crates/nu-cmd-plugin/src/util.rs new file mode 100644 index 0000000..223e93b --- /dev/null +++ b/nushell/crates/nu-cmd-plugin/src/util.rs @@ -0,0 +1,145 @@ +#[allow(deprecated)] +use nu_engine::{command_prelude::*, current_dir}; +use nu_protocol::{ + PluginRegistryFile, + engine::StateWorkingSet, + shell_error::{self, io::IoError}, +}; +use std::{ + fs::{self, File}, + path::PathBuf, +}; + +fn get_plugin_registry_file_path( + engine_state: &EngineState, + stack: &mut Stack, + span: Span, + custom_path: &Option>, +) -> Result { + #[allow(deprecated)] + let cwd = current_dir(engine_state, stack)?; + + if let Some(custom_path) = custom_path { + Ok(nu_path::expand_path_with(&custom_path.item, cwd, true)) + } else { + engine_state + .plugin_path + .clone() + .ok_or_else(|| ShellError::GenericError { + error: "Plugin registry file not set".into(), + msg: "pass --plugin-config explicitly here".into(), + span: Some(span), + help: Some("you may be running `nu` with --no-config-file".into()), + inner: vec![], + }) + } +} + +pub(crate) fn read_plugin_file( + engine_state: &EngineState, + stack: &mut Stack, + span: Span, + custom_path: &Option>, +) -> Result { + let plugin_registry_file_path = + get_plugin_registry_file_path(engine_state, stack, span, custom_path)?; + + let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span); + + // Try to read the plugin file if it exists + if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) { + PluginRegistryFile::read_from( + File::open(&plugin_registry_file_path) + .map_err(|err| IoError::new(err, file_span, plugin_registry_file_path))?, + Some(file_span), + ) + } else if let Some(path) = custom_path { + Err(ShellError::Io(IoError::new( + shell_error::io::ErrorKind::FileNotFound, + path.span, + PathBuf::from(&path.item), + ))) + } else { + Ok(PluginRegistryFile::default()) + } +} + +pub(crate) fn modify_plugin_file( + engine_state: &EngineState, + stack: &mut Stack, + span: Span, + custom_path: &Option>, + operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>, +) -> Result<(), ShellError> { + let plugin_registry_file_path = + get_plugin_registry_file_path(engine_state, stack, span, custom_path)?; + + let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span); + + // Try to read the plugin file if it exists + let mut contents = if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) { + PluginRegistryFile::read_from( + File::open(&plugin_registry_file_path) + .map_err(|err| IoError::new(err, file_span, plugin_registry_file_path.clone()))?, + Some(file_span), + )? + } else { + PluginRegistryFile::default() + }; + + // Do the operation + operate(&mut contents)?; + + // Save the modified file on success + contents.write_to( + File::create(&plugin_registry_file_path) + .map_err(|err| IoError::new(err, file_span, plugin_registry_file_path))?, + Some(span), + )?; + + Ok(()) +} + +pub(crate) fn canonicalize_possible_filename_arg( + engine_state: &EngineState, + stack: &Stack, + arg: &str, +) -> PathBuf { + // This results in the best possible chance of a match with the plugin item + #[allow(deprecated)] + if let Ok(cwd) = nu_engine::current_dir(engine_state, stack) { + let path = nu_path::expand_path_with(arg, &cwd, true); + // Try to canonicalize + nu_path::locate_in_dirs(&path, &cwd, || get_plugin_dirs(engine_state, stack)) + // If we couldn't locate it, return the expanded path alone + .unwrap_or(path) + } else { + arg.into() + } +} + +pub(crate) fn get_plugin_dirs( + engine_state: &EngineState, + stack: &Stack, +) -> impl Iterator { + // Get the NU_PLUGIN_DIRS from the constant and/or env var + let working_set = StateWorkingSet::new(engine_state); + let dirs_from_const = working_set + .find_variable(b"$NU_PLUGIN_DIRS") + .and_then(|var_id| working_set.get_constant(var_id).ok()) + .cloned() // TODO: avoid this clone + .into_iter() + .flat_map(|value| value.into_list().ok()) + .flatten() + .flat_map(|list_item| list_item.coerce_into_string().ok()); + + let dirs_from_env = stack + .get_env_var(engine_state, "NU_PLUGIN_DIRS") + .cloned() // TODO: avoid this clone + .into_iter() + .flat_map(|value| value.into_list().ok()) + .flatten() + .flat_map(|list_item| list_item.coerce_into_string().ok()); + + dirs_from_const.chain(dirs_from_env) +} diff --git a/nushell/crates/nu-color-config/Cargo.toml b/nushell/crates/nu-color-config/Cargo.toml new file mode 100644 index 0000000..6b3aa21 --- /dev/null +++ b/nushell/crates/nu-color-config/Cargo.toml @@ -0,0 +1,25 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Color configuration code used by Nushell" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-color-config" +edition = "2024" +license = "MIT" +name = "nu-color-config" +version = "0.105.2" + +[lib] +bench = false + +[lints] +workspace = true + +[dependencies] +nu-protocol = { path = "../nu-protocol", version = "0.105.2", default-features = false } +nu-engine = { path = "../nu-engine", version = "0.105.2", default-features = false } +nu-json = { path = "../nu-json", version = "0.105.2" } +nu-ansi-term = { workspace = true } + +serde = { workspace = true, features = ["derive"] } + +[dev-dependencies] +nu-test-support = { path = "../nu-test-support", version = "0.105.2" } diff --git a/nushell/crates/nu-color-config/LICENSE b/nushell/crates/nu-color-config/LICENSE new file mode 100644 index 0000000..ae174e8 --- /dev/null +++ b/nushell/crates/nu-color-config/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nushell/crates/nu-color-config/README.md b/nushell/crates/nu-color-config/README.md new file mode 100644 index 0000000..f037536 --- /dev/null +++ b/nushell/crates/nu-color-config/README.md @@ -0,0 +1,5 @@ +Logic to resolve colors for syntax highlighting and output formatting + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/nushell/crates/nu-color-config/src/color_config.rs b/nushell/crates/nu-color-config/src/color_config.rs new file mode 100644 index 0000000..11db4c8 --- /dev/null +++ b/nushell/crates/nu-color-config/src/color_config.rs @@ -0,0 +1,159 @@ +use crate::{ + NuStyle, + nu_style::{color_from_hex, lookup_style}, + parse_nustyle, +}; +use nu_ansi_term::Style; +use nu_protocol::{Record, Value}; +use std::collections::HashMap; + +pub fn lookup_ansi_color_style(s: &str) -> Style { + if s.starts_with('#') { + color_from_hex(s) + .ok() + .and_then(|c| c.map(|c| c.normal())) + .unwrap_or_default() + } else if s.starts_with('{') { + color_string_to_nustyle(s) + } else { + lookup_style(s) + } +} + +pub fn get_color_map(colors: &HashMap) -> HashMap { + let mut hm: HashMap = HashMap::new(); + + for (key, value) in colors { + parse_map_entry(&mut hm, key, value); + } + + hm +} + +fn parse_map_entry(hm: &mut HashMap, key: &str, value: &Value) { + let value = match value { + Value::String { val, .. } => Some(lookup_ansi_color_style(val)), + Value::Record { val, .. } => get_style_from_value(val).map(parse_nustyle), + _ => None, + }; + if let Some(value) = value { + hm.entry(key.to_owned()).or_insert(value); + } +} + +fn get_style_from_value(record: &Record) -> Option { + let mut was_set = false; + let mut style = NuStyle::from(Style::default()); + for (col, val) in record { + match col.as_str() { + "bg" => { + if let Value::String { val, .. } = val { + style.bg = Some(val.clone()); + was_set = true; + } + } + "fg" => { + if let Value::String { val, .. } = val { + style.fg = Some(val.clone()); + was_set = true; + } + } + "attr" => { + if let Value::String { val, .. } = val { + style.attr = Some(val.clone()); + was_set = true; + } + } + _ => (), + } + } + + if was_set { Some(style) } else { None } +} + +fn color_string_to_nustyle(color_string: &str) -> Style { + // eprintln!("color_string: {}", &color_string); + if color_string.is_empty() { + return Style::default(); + } + + let nu_style = match nu_json::from_str::(color_string) { + Ok(s) => s, + Err(_) => return Style::default(), + }; + + parse_nustyle(nu_style) +} + +#[cfg(test)] +mod tests { + use super::*; + use nu_ansi_term::{Color, Style}; + use nu_protocol::{Span, Value, record}; + + #[test] + fn test_color_string_to_nustyle_empty_string() { + let color_string = String::new(); + let style = color_string_to_nustyle(&color_string); + assert_eq!(style, Style::default()); + } + + #[test] + fn test_color_string_to_nustyle_valid_string() { + let color_string = r#"{"fg": "black", "bg": "white", "attr": "b"}"#; + let style = color_string_to_nustyle(color_string); + assert_eq!(style.foreground, Some(Color::Black)); + assert_eq!(style.background, Some(Color::White)); + assert!(style.is_bold); + } + + #[test] + fn test_color_string_to_nustyle_invalid_string() { + let color_string = "invalid string"; + let style = color_string_to_nustyle(color_string); + assert_eq!(style, Style::default()); + } + + #[test] + fn test_get_style_from_value() { + // Test case 1: all values are valid + let record = record! { + "bg" => Value::test_string("red"), + "fg" => Value::test_string("blue"), + "attr" => Value::test_string("bold"), + }; + let expected_style = NuStyle { + bg: Some("red".to_string()), + fg: Some("blue".to_string()), + attr: Some("bold".to_string()), + }; + assert_eq!(get_style_from_value(&record), Some(expected_style)); + + // Test case 2: no values are valid + let record = record! { + "invalid" => Value::nothing(Span::test_data()), + }; + assert_eq!(get_style_from_value(&record), None); + + // Test case 3: some values are valid + let record = record! { + "bg" => Value::test_string("green"), + "invalid" => Value::nothing(Span::unknown()), + }; + let expected_style = NuStyle { + bg: Some("green".to_string()), + fg: None, + attr: None, + }; + assert_eq!(get_style_from_value(&record), Some(expected_style)); + } + + #[test] + fn test_parse_map_entry() { + let mut hm = HashMap::new(); + let key = "test_key".to_owned(); + let value = Value::string("red", Span::unknown()); + parse_map_entry(&mut hm, &key, &value); + assert_eq!(hm.get(&key), Some(&lookup_ansi_color_style("red"))); + } +} diff --git a/nushell/crates/nu-color-config/src/lib.rs b/nushell/crates/nu-color-config/src/lib.rs new file mode 100644 index 0000000..77ec9d0 --- /dev/null +++ b/nushell/crates/nu-color-config/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!("../README.md")] +mod color_config; +mod matching_brackets_style; +mod nu_style; +mod shape_color; +mod style_computer; +mod text_style; + +pub use color_config::*; +pub use matching_brackets_style::*; +pub use nu_style::*; +pub use shape_color::*; +pub use style_computer::*; +pub use text_style::*; diff --git a/nushell/crates/nu-color-config/src/matching_brackets_style.rs b/nushell/crates/nu-color-config/src/matching_brackets_style.rs new file mode 100644 index 0000000..85ac23a --- /dev/null +++ b/nushell/crates/nu-color-config/src/matching_brackets_style.rs @@ -0,0 +1,31 @@ +use crate::color_config::lookup_ansi_color_style; +use nu_ansi_term::Style; +use nu_protocol::Config; + +pub fn get_matching_brackets_style(default_style: Style, conf: &Config) -> Style { + const MATCHING_BRACKETS_CONFIG_KEY: &str = "shape_matching_brackets"; + + match conf.color_config.get(MATCHING_BRACKETS_CONFIG_KEY) { + Some(int_color) => match int_color.coerce_str() { + Ok(int_color) => merge_styles(default_style, lookup_ansi_color_style(&int_color)), + Err(_) => default_style, + }, + None => default_style, + } +} + +fn merge_styles(base: Style, extra: Style) -> Style { + Style { + foreground: extra.foreground.or(base.foreground), + background: extra.background.or(base.background), + is_bold: extra.is_bold || base.is_bold, + is_dimmed: extra.is_dimmed || base.is_dimmed, + is_italic: extra.is_italic || base.is_italic, + is_underline: extra.is_underline || base.is_underline, + is_blink: extra.is_blink || base.is_blink, + is_reverse: extra.is_reverse || base.is_reverse, + is_hidden: extra.is_hidden || base.is_hidden, + is_strikethrough: extra.is_strikethrough || base.is_strikethrough, + prefix_with_reset: false, + } +} diff --git a/nushell/crates/nu-color-config/src/nu_style.rs b/nushell/crates/nu-color-config/src/nu_style.rs new file mode 100644 index 0000000..578534a --- /dev/null +++ b/nushell/crates/nu-color-config/src/nu_style.rs @@ -0,0 +1,599 @@ +use nu_ansi_term::{Color, Style}; +use nu_protocol::Value; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)] +pub struct NuStyle { + pub fg: Option, + pub bg: Option, + pub attr: Option, +} + +impl From3" + ); +} + +#[test] +fn out_html_metadata() { + let actual = nu!(r#" + echo 3 | to html | metadata | get content_type + "#); + + assert_eq!(actual.out, r#"text/html; charset=utf-8"#); +} + +#[test] +fn out_html_partial() { + let actual = nu!(r#" + echo 3 | to html -p + "#); + + assert_eq!( + actual.out, + "
3
" + ); +} + +#[test] +fn out_html_table() { + let actual = nu!(r#" + echo '{"name": "darren"}' | from json | to html + "#); + + assert_eq!( + actual.out, + r"
name
darren
" + ); +} + +#[test] +#[ignore] +fn test_cd_html_color_flag_dark_false() { + let actual = nu!(r#" + cd --help | to html --html-color + "#); + assert_eq!( + actual.out, + r"Change directory.

Usage:
> cd (path)

Flags:
-h, --help - Display the help message for this command

Signatures:
<nothing> | cd <string?> -> <nothing>
<string> | cd <string?> -> <nothing>

Parameters:
(optional)
path <directory>: the path to change to

Examples:
Change to your home directory
>
cd
~

Change to a directory via abbreviations
>
cd
d/s/9

Change to the previous working directory ($OLDPWD)
>
cd
-

" + ); +} + +#[test] +#[ignore] +fn test_no_color_flag() { + // TODO replace with something potentially more stable, otherwise this test needs to be + // manuallly updated when ever the help output changes + let actual = nu!(r#" + cd --help | to html --no-color + "#); + assert_eq!( + actual.out, + r"Change directory.

Usage:
> cd (path)

Flags:
-h, --help - Display the help message for this command

Parameters:
path <directory>: The path to change to. (optional)

Input/output types:
╭─#─┬──input──┬─output──╮
│ 0 │ nothing │ nothing │
│ 1 │ string │ nothing │
╰───┴─────────┴─────────╯

Examples:
Change to your home directory
> cd ~

Change to the previous working directory ($OLDPWD)
> cd -

" + ) +} + +#[test] +fn test_list() { + let actual = nu!(r#"to html --list | where name == C64 | get 0 | to nuon"#); + assert_eq!( + actual.out, + r##"{name: "C64", black: "#090300", red: "#883932", green: "#55a049", yellow: "#bfce72", blue: "#40318d", purple: "#8b3f96", cyan: "#67b6bd", white: "#ffffff", brightBlack: "#000000", brightRed: "#883932", brightGreen: "#55a049", brightYellow: "#bfce72", brightBlue: "#40318d", brightPurple: "#8b3f96", brightCyan: "#67b6bd", brightWhite: "#f7f7f7", background: "#40318d", foreground: "#7869c4"}"## + ); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/json.rs b/nushell/crates/nu-command/tests/format_conversions/json.rs new file mode 100644 index 0000000..9b6c045 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/json.rs @@ -0,0 +1,299 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn table_to_json_text_and_from_json_text_back_into_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sgml_description.json + | to json + | from json + | get glossary.GlossDiv.GlossList.GlossEntry.GlossSee + "# + )); + + assert_eq!(actual.out, "markup"); +} + +#[test] +fn from_json_text_to_table() { + Playground::setup("filter_from_json_test_1", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "katz.txt", + r#" + { + "katz": [ + {"name": "Yehuda", "rusty_luck": 1}, + {"name": "JT", "rusty_luck": 1}, + {"name": "Andres", "rusty_luck": 1}, + {"name":"GorbyPuff", "rusty_luck": 1} + ] + } + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open katz.txt | from json | get katz | get rusty_luck | length " + ); + + assert_eq!(actual.out, "4"); + }) +} + +#[test] +fn from_json_text_to_table_strict() { + Playground::setup("filter_from_json_test_1_strict", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "katz.txt", + r#" + { + "katz": [ + {"name": "Yehuda", "rusty_luck": 1}, + {"name": "JT", "rusty_luck": 1}, + {"name": "Andres", "rusty_luck": 1}, + {"name":"GorbyPuff", "rusty_luck": 1} + ] + } + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open katz.txt | from json -s | get katz | get rusty_luck | length " + ); + + assert_eq!(actual.out, "4"); + }) +} + +#[test] +fn from_json_text_recognizing_objects_independently_to_table() { + Playground::setup("filter_from_json_test_2", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "katz.txt", + r#" + {"name": "Yehuda", "rusty_luck": 1} + {"name": "JT", "rusty_luck": 1} + {"name": "Andres", "rusty_luck": 1} + {"name":"GorbyPuff", "rusty_luck": 3} + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open katz.txt + | from json -o + | where name == "GorbyPuff" + | get rusty_luck.0 + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn from_json_text_objects_is_stream() { + Playground::setup("filter_from_json_test_2_is_stream", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "katz.txt", + r#" + {"name": "Yehuda", "rusty_luck": 1} + {"name": "JT", "rusty_luck": 1} + {"name": "Andres", "rusty_luck": 1} + {"name":"GorbyPuff", "rusty_luck": 3} + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open katz.txt + | from json -o + | describe -n + "# + )); + + assert_eq!(actual.out, "stream"); + }) +} + +#[test] +fn from_json_text_recognizing_objects_independently_to_table_strict() { + Playground::setup("filter_from_json_test_2_strict", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "katz.txt", + r#" + {"name": "Yehuda", "rusty_luck": 1} + {"name": "JT", "rusty_luck": 1} + {"name": "Andres", "rusty_luck": 1} + {"name":"GorbyPuff", "rusty_luck": 3} + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open katz.txt + | from json -o -s + | where name == "GorbyPuff" + | get rusty_luck.0 + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn table_to_json_text() { + Playground::setup("filter_to_json_test", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "sample.txt", + r#" + JonAndrehudaTZ,3 + GorbyPuff,100 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.txt + | lines + | split column "," name luck + | select name + | to json + | from json + | get 0 + | get name + "# + )); + + assert_eq!(actual.out, "JonAndrehudaTZ"); + }) +} + +#[test] +fn table_to_json_text_strict() { + Playground::setup("filter_to_json_test_strict", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "sample.txt", + r#" + JonAndrehudaTZ,3 + GorbyPuff,100 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.txt + | lines + | split column "," name luck + | select name + | to json + | from json -s + | get 0 + | get name + "# + )); + + assert_eq!(actual.out, "JonAndrehudaTZ"); + }) +} + +#[test] +fn top_level_values_from_json() { + for (value, type_name) in [("null", "nothing"), ("true", "bool"), ("false", "bool")] { + let actual = nu!(r#""{}" | from json | to json"#, value); + assert_eq!(actual.out, value); + let actual = nu!(r#""{}" | from json | describe"#, value); + assert_eq!(actual.out, type_name); + } +} + +#[test] +fn top_level_values_from_json_strict() { + for (value, type_name) in [("null", "nothing"), ("true", "bool"), ("false", "bool")] { + let actual = nu!(r#""{}" | from json -s | to json"#, value); + assert_eq!(actual.out, value); + let actual = nu!(r#""{}" | from json -s | describe"#, value); + assert_eq!(actual.out, type_name); + } +} + +#[test] +fn strict_parsing_fails_on_comment() { + let actual = nu!(r#"'{ "a": 1, /* comment */ "b": 2 }' | from json -s"#); + assert!(actual.err.contains("error parsing JSON text")); +} + +#[test] +fn strict_parsing_fails_on_trailing_comma() { + let actual = nu!(r#"'{ "a": 1, "b": 2, }' | from json -s"#); + assert!(actual.err.contains("error parsing JSON text")); +} + +#[test] +fn ranges_to_json_as_array() { + let value = r#"[ 1, 2, 3]"#; + let actual = nu!(r#"1..3 | to json"#); + assert_eq!(actual.out, value); +} + +#[test] +fn unbounded_from_in_range_fails() { + let actual = nu!(r#"1.. | to json"#); + assert!(actual.err.contains("Cannot create range")); +} + +#[test] +fn inf_in_range_fails() { + let actual = nu!(r#"inf..5 | to json"#); + assert!(actual.err.contains("can't convert to countable values")); + let actual = nu!(r#"5..inf | to json"#); + assert!( + actual + .err + .contains("Unbounded ranges are not allowed when converting to this format") + ); + let actual = nu!(r#"-inf..inf | to json"#); + assert!(actual.err.contains("can't convert to countable values")); +} + +#[test] +fn test_indent_flag() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '{ "a": 1, "b": 2, "c": 3 }' + | from json + | to json --indent 3 + "# + )); + + let expected_output = "{ \"a\": 1, \"b\": 2, \"c\": 3}"; + + assert_eq!(actual.out, expected_output); +} + +#[test] +fn test_tabs_indent_flag() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '{ "a": 1, "b": 2, "c": 3 }' + | from json + | to json --tabs 2 + "# + )); + + let expected_output = "{\t\t\"a\": 1,\t\t\"b\": 2,\t\t\"c\": 3}"; + + assert_eq!(actual.out, expected_output); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/markdown.rs b/nushell/crates/nu-command/tests/format_conversions/markdown.rs new file mode 100644 index 0000000..9fefaf5 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/markdown.rs @@ -0,0 +1,79 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn md_empty() { + let actual = nu!(r#" + echo [[]; []] | from json | to md + "#); + + assert_eq!(actual.out, ""); +} + +#[test] +fn md_empty_pretty() { + let actual = nu!(r#" + echo "{}" | from json | to md -p + "#); + + assert_eq!(actual.out, ""); +} + +#[test] +fn md_simple() { + let actual = nu!(r#" + echo 3 | to md + "#); + + assert_eq!(actual.out, "3"); +} + +#[test] +fn md_simple_pretty() { + let actual = nu!(r#" + echo 3 | to md -p + "#); + + assert_eq!(actual.out, "3"); +} + +#[test] +fn md_table() { + let actual = nu!(r#" + echo [[name]; [jason]] | to md + "#); + + assert_eq!(actual.out, "|name||-||jason|"); +} + +#[test] +fn md_table_pretty() { + let actual = nu!(r#" + echo [[name]; [joseph]] | to md -p + "#); + + assert_eq!(actual.out, "| name || ------ || joseph |"); +} + +#[test] +fn md_combined() { + let actual = nu!(pipeline( + r#" + def title [] { + echo [[H1]; ["Nu top meals"]] + }; + + def meals [] { + echo [[dish]; [Arepa] [Taco] [Pizza]] + }; + + title + | append (meals) + | to md --per-element --pretty + "# + )); + + assert_eq!( + actual.out, + "# Nu top meals| dish || ----- || Arepa || Taco || Pizza |" + ); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/mod.rs b/nushell/crates/nu-command/tests/format_conversions/mod.rs new file mode 100644 index 0000000..939a55d --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/mod.rs @@ -0,0 +1,15 @@ +mod csv; +mod html; +mod json; +mod markdown; +mod msgpack; +mod msgpackz; +mod nuon; +mod ods; +mod ssv; +mod toml; +mod tsv; +mod url; +mod xlsx; +mod xml; +mod yaml; diff --git a/nushell/crates/nu-command/tests/format_conversions/msgpack.rs b/nushell/crates/nu-command/tests/format_conversions/msgpack.rs new file mode 100644 index 0000000..578e96e --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/msgpack.rs @@ -0,0 +1,161 @@ +use nu_test_support::{nu, playground::Playground}; +use pretty_assertions::assert_eq; + +fn msgpack_test(fixture_name: &str, commands: Option<&str>) -> nu_test_support::Outcome { + let path_to_generate_nu = nu_test_support::fs::fixtures() + .join("formats") + .join("msgpack") + .join("generate.nu"); + + let mut outcome = None; + Playground::setup(&format!("msgpack test {}", fixture_name), |dirs, _| { + assert!( + nu!( + cwd: dirs.test(), + format!( + "nu -n '{}' '{}'", + path_to_generate_nu.display(), + fixture_name + ), + ) + .status + .success() + ); + + outcome = Some(nu!( + cwd: dirs.test(), + collapse_output: false, + commands.map(|c| c.to_owned()).unwrap_or_else(|| format!("open {fixture_name}.msgpack")) + )); + }); + outcome.expect("failed to get outcome") +} + +fn msgpack_nuon_test(fixture_name: &str, opts: &str) { + let path_to_nuon = nu_test_support::fs::fixtures() + .join("formats") + .join("msgpack") + .join(format!("{fixture_name}.nuon")); + + let sample_nuon = std::fs::read_to_string(path_to_nuon).expect("failed to open nuon file"); + + let outcome = msgpack_test( + fixture_name, + Some(&format!( + "open --raw {fixture_name}.msgpack | from msgpack {opts} | to nuon --indent 4" + )), + ); + + assert!(outcome.status.success()); + assert!(outcome.err.is_empty()); + assert_eq!( + sample_nuon.replace("\r\n", "\n"), + outcome.out.replace("\r\n", "\n") + ); +} + +#[test] +fn sample() { + msgpack_nuon_test("sample", ""); +} + +#[test] +fn sample_roundtrip() { + let path_to_sample_nuon = nu_test_support::fs::fixtures() + .join("formats") + .join("msgpack") + .join("sample.nuon"); + + let sample_nuon = + std::fs::read_to_string(&path_to_sample_nuon).expect("failed to open sample.nuon"); + + let outcome = nu!( + collapse_output: false, + format!( + "open '{}' | to msgpack | from msgpack | to nuon --indent 4", + path_to_sample_nuon.display() + ) + ); + + assert!(outcome.status.success()); + assert!(outcome.err.is_empty()); + assert_eq!( + sample_nuon.replace("\r\n", "\n"), + outcome.out.replace("\r\n", "\n") + ); +} + +#[test] +fn objects() { + msgpack_nuon_test("objects", "--objects"); +} + +#[test] +fn max_depth() { + let outcome = msgpack_test("max-depth", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("exceeded depth limit")); +} + +#[test] +fn non_utf8() { + let outcome = msgpack_test("non-utf8", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("utf-8")); +} + +#[test] +fn empty() { + let outcome = msgpack_test("empty", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("fill whole buffer")); +} + +#[test] +fn eof() { + let outcome = msgpack_test("eof", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("fill whole buffer")); +} + +#[test] +fn after_eof() { + let outcome = msgpack_test("after-eof", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("after end of")); +} + +#[test] +fn reserved() { + let outcome = msgpack_test("reserved", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("Reserved")); +} + +#[test] +fn u64_too_large() { + let outcome = msgpack_test("u64-too-large", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("integer too big")); +} + +#[test] +fn non_string_map_key() { + let outcome = msgpack_test("non-string-map-key", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("string key")); +} + +#[test] +fn timestamp_wrong_length() { + let outcome = msgpack_test("timestamp-wrong-length", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("Unknown MessagePack extension")); +} + +#[test] +fn other_extension_type() { + let outcome = msgpack_test("other-extension-type", None); + assert!(!outcome.status.success()); + assert!(outcome.err.contains("Unknown MessagePack extension")); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/msgpackz.rs b/nushell/crates/nu-command/tests/format_conversions/msgpackz.rs new file mode 100644 index 0000000..c2a7ade --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/msgpackz.rs @@ -0,0 +1,28 @@ +use nu_test_support::nu; +use pretty_assertions::assert_eq; + +#[test] +fn sample_roundtrip() { + let path_to_sample_nuon = nu_test_support::fs::fixtures() + .join("formats") + .join("msgpack") + .join("sample.nuon"); + + let sample_nuon = + std::fs::read_to_string(&path_to_sample_nuon).expect("failed to open sample.nuon"); + + let outcome = nu!( + collapse_output: false, + format!( + "open '{}' | to msgpackz | from msgpackz | to nuon --indent 4", + path_to_sample_nuon.display() + ) + ); + + assert!(outcome.status.success()); + assert!(outcome.err.is_empty()); + assert_eq!( + sample_nuon.replace("\r\n", "\n"), + outcome.out.replace("\r\n", "\n") + ); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/nuon.rs b/nushell/crates/nu-command/tests/format_conversions/nuon.rs new file mode 100644 index 0000000..cdac532 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/nuon.rs @@ -0,0 +1,524 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn to_nuon_correct_compaction() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open appveyor.yml + | to nuon + | str length + | $in > 500 + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn to_nuon_list_of_numbers() { + let actual = nu!(pipeline( + r#" + [1, 2, 3, 4] + | to nuon + | from nuon + | $in == [1, 2, 3, 4] + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn to_nuon_list_of_strings() { + let actual = nu!(pipeline( + r#" + [abc, xyz, def] + | to nuon + | from nuon + | $in == [abc, xyz, def] + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn to_nuon_table() { + let actual = nu!(pipeline( + r#" + [[my, columns]; [abc, xyz], [def, ijk]] + | to nuon + | from nuon + | $in == [[my, columns]; [abc, xyz], [def, ijk]] + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn from_nuon_illegal_table() { + let actual = nu!(pipeline( + r#" + "[[repeated repeated]; [abc, xyz], [def, ijk]]" + | from nuon + "# + )); + + assert!(actual.err.contains("column_defined_twice")); +} + +#[test] +fn to_nuon_bool() { + let actual = nu!(pipeline( + r#" + false + | to nuon + | from nuon + "# + )); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn to_nuon_escaping() { + let actual = nu!(pipeline( + r#" + "hello\"world" + | to nuon + | from nuon + "# + )); + + assert_eq!(actual.out, "hello\"world"); +} + +#[test] +fn to_nuon_escaping2() { + let actual = nu!(pipeline( + r#" + "hello\\world" + | to nuon + | from nuon + "# + )); + + assert_eq!(actual.out, "hello\\world"); +} + +#[test] +fn to_nuon_escaping3() { + let actual = nu!(pipeline( + r#" + ["hello\\world"] + | to nuon + | from nuon + | $in == [hello\world] + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn to_nuon_escaping4() { + let actual = nu!(pipeline( + r#" + ["hello\"world"] + | to nuon + | from nuon + | $in == ["hello\"world"] + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn to_nuon_escaping5() { + let actual = nu!(pipeline( + r#" + {s: "hello\"world"} + | to nuon + | from nuon + | $in == {s: "hello\"world"} + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn to_nuon_negative_int() { + let actual = nu!(pipeline( + r#" + -1 + | to nuon + | from nuon + "# + )); + + assert_eq!(actual.out, "-1"); +} + +#[test] +fn to_nuon_records() { + let actual = nu!(pipeline( + r#" + {name: "foo bar", age: 100, height: 10} + | to nuon + | from nuon + | $in == {name: "foo bar", age: 100, height: 10} + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn to_nuon_range() { + let actual = nu!(r#"1..42 | to nuon"#); + assert_eq!(actual.out, "1..42"); + + let actual = nu!(r#"1..<42 | to nuon"#); + assert_eq!(actual.out, "1..<42"); + + let actual = nu!(r#"1..4..42 | to nuon"#); + assert_eq!(actual.out, "1..4..42"); + + let actual = nu!(r#"1..4..<42 | to nuon"#); + assert_eq!(actual.out, "1..4..<42"); + + let actual = nu!(r#"1.0..42.0 | to nuon"#); + assert_eq!(actual.out, "1.0..42.0"); + + let actual = nu!(r#"1.0..<42.0 | to nuon"#); + assert_eq!(actual.out, "1.0..<42.0"); + + let actual = nu!(r#"1.0..4.0..42.0 | to nuon"#); + assert_eq!(actual.out, "1.0..4.0..42.0"); + + let actual = nu!(r#"1.0..4.0..<42.0 | to nuon"#); + assert_eq!(actual.out, "1.0..4.0..<42.0"); +} + +#[test] +fn from_nuon_range() { + let actual = nu!(r#"'1..42' | from nuon | to nuon"#); + assert_eq!(actual.out, "1..42"); + + let actual = nu!(r#"'1..<42' | from nuon | to nuon"#); + assert_eq!(actual.out, "1..<42"); + + let actual = nu!(r#"'1..4..42' | from nuon | to nuon"#); + assert_eq!(actual.out, "1..4..42"); + + let actual = nu!(r#"'1..4..<42' | from nuon | to nuon"#); + assert_eq!(actual.out, "1..4..<42"); + + let actual = nu!(r#"'1.0..42.0' | from nuon | to nuon"#); + assert_eq!(actual.out, "1.0..42.0"); + + let actual = nu!(r#"'1.0..<42.0' | from nuon | to nuon"#); + assert_eq!(actual.out, "1.0..<42.0"); + + let actual = nu!(r#"'1.0..4.0..42.0' | from nuon | to nuon"#); + assert_eq!(actual.out, "1.0..4.0..42.0"); + + let actual = nu!(r#"'1.0..4.0..<42.0' | from nuon | to nuon"#); + assert_eq!(actual.out, "1.0..4.0..<42.0"); +} + +#[test] +fn to_nuon_filesize() { + let actual = nu!(pipeline( + r#" + 1kib + | to nuon + "# + )); + + assert_eq!(actual.out, "1024b"); +} + +#[test] +fn from_nuon_filesize() { + let actual = nu!(pipeline( + r#" + "1024b" + | from nuon + | describe + "# + )); + + assert_eq!(actual.out, "filesize"); +} + +#[test] +fn to_nuon_duration() { + let actual = nu!(pipeline( + r#" + 1min + | to nuon + "# + )); + + assert_eq!(actual.out, "60000000000ns"); +} + +#[test] +fn from_nuon_duration() { + let actual = nu!(pipeline( + r#" + "60000000000ns" + | from nuon + | describe + "# + )); + + assert_eq!(actual.out, "duration"); +} + +#[test] +fn to_nuon_datetime() { + let actual = nu!(pipeline( + r#" + 2019-05-10 + | to nuon + "# + )); + + assert_eq!(actual.out, "2019-05-10T00:00:00+00:00"); +} + +#[test] +fn from_nuon_datetime() { + let actual = nu!(pipeline( + r#" + "2019-05-10T00:00:00+00:00" + | from nuon + | describe + "# + )); + + assert_eq!(actual.out, "datetime"); +} + +#[test] +fn to_nuon_errs_on_closure() { + let actual = nu!(pipeline( + r#" + {|| to nuon} + | to nuon + "# + )); + + assert!(actual.err.contains("not deserializable")); +} + +#[test] +fn to_nuon_closure_coerced_to_quoted_string() { + let actual = nu!(pipeline( + r#" + {|| to nuon} + | to nuon --serialize + "# + )); + + assert_eq!(actual.out, "\"{|| to nuon}\""); +} + +#[test] +fn binary_to() { + let actual = nu!(pipeline( + r#" + 0x[ab cd ef] | to nuon + "# + )); + + assert_eq!(actual.out, "0x[ABCDEF]"); +} + +#[test] +fn binary_roundtrip() { + let actual = nu!(pipeline( + r#" + "0x[1f ff]" | from nuon | to nuon + "# + )); + + assert_eq!(actual.out, "0x[1FFF]"); +} + +#[test] +fn read_binary_data() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.nuon | get 5.3 + "# + )); + + assert_eq!(actual.out, "31") +} + +#[test] +fn read_record() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.nuon | get 4.name + "# + )); + + assert_eq!(actual.out, "Bobby") +} + +#[test] +fn read_bool() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.nuon | get 3 | $in == true + "# + )); + + assert_eq!(actual.out, "true") +} + +#[test] +fn float_doesnt_become_int() { + let actual = nu!(pipeline( + r#" + 1.0 | to nuon + "# + )); + + assert_eq!(actual.out, "1.0") +} + +#[test] +fn float_inf_parsed_properly() { + let actual = nu!(pipeline( + r#" + inf | to nuon + "# + )); + + assert_eq!(actual.out, "inf") +} + +#[test] +fn float_neg_inf_parsed_properly() { + let actual = nu!(pipeline( + r#" + -inf | to nuon + "# + )); + + assert_eq!(actual.out, "-inf") +} + +#[test] +fn float_nan_parsed_properly() { + let actual = nu!(pipeline( + r#" + NaN | to nuon + "# + )); + + assert_eq!(actual.out, "NaN") +} + +#[test] +fn to_nuon_converts_columns_with_spaces() { + let actual = nu!(pipeline( + r#" + let test = [[a, b, "c d"]; [1 2 3] [4 5 6]]; $test | to nuon | from nuon + "# + )); + assert!(actual.err.is_empty()); +} + +#[test] +fn to_nuon_quotes_empty_string() { + let actual = nu!(pipeline( + r#" + let test = ""; $test | to nuon + "# + )); + assert!(actual.err.is_empty()); + assert_eq!(actual.out, r#""""#) +} + +#[test] +fn to_nuon_quotes_empty_string_in_list() { + let actual = nu!(pipeline( + r#" + let test = [""]; $test | to nuon | from nuon | $in == [""] + "# + )); + assert!(actual.err.is_empty()); + assert_eq!(actual.out, "true") +} + +#[test] +fn to_nuon_quotes_empty_string_in_table() { + let actual = nu!(pipeline( + r#" + let test = [[a, b]; ['', la] [le lu]]; $test | to nuon | from nuon + "# + )); + assert!(actual.err.is_empty()); +} + +#[test] +fn does_not_quote_strings_unnecessarily() { + let actual = nu!(pipeline( + r#" + let test = [["a", "b", "c d"]; [1 2 3] [4 5 6]]; $test | to nuon + "# + )); + assert_eq!(actual.out, "[[a, b, \"c d\"]; [1, 2, 3], [4, 5, 6]]"); + let actual = nu!(pipeline( + r#" + let a = {"ro name": "sam" rank: 10}; $a | to nuon + "# + )); + assert_eq!(actual.out, "{\"ro name\": sam, rank: 10}"); +} + +#[test] +fn quotes_some_strings_necessarily() { + let actual = nu!(pipeline( + r#" + ['true','false','null', + 'NaN','NAN','nan','+nan','-nan', + 'inf','+inf','-inf','INF', + 'Infinity','+Infinity','-Infinity','INFINITY', + '+19.99','-19.99', '19.99b', + '19.99kb','19.99mb','19.99gb','19.99tb','19.99pb','19.99eb','19.99zb', + '19.99kib','19.99mib','19.99gib','19.99tib','19.99pib','19.99eib','19.99zib', + '19ns', '19us', '19ms', '19sec', '19min', '19hr', '19day', '19wk', + '-11.0..-15.0', '11.0..-15.0', '-11.0..15.0', + '-11.0..<-15.0', '11.0..<-15.0', '-11.0..<15.0', + '-11.0..', '11.0..', '..15.0', '..-15.0', '..<15.0', '..<-15.0', + '2000-01-01', '2022-02-02T14:30:00', '2022-02-02T14:30:00+05:00', + ',','' + '&&' + ] | to nuon | from nuon | describe + "# + )); + + assert_eq!(actual.out, "list"); +} + +#[test] +fn read_code_should_fail_rather_than_panic() { + let actual = nu!(cwd: "tests/fixtures/formats", pipeline( + r#"open code.nu | from nuon"# + )); + assert!(actual.err.contains("Error when loading")) +} diff --git a/nushell/crates/nu-command/tests/format_conversions/ods.rs b/nushell/crates/nu-command/tests/format_conversions/ods.rs new file mode 100644 index 0000000..8a69493 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/ods.rs @@ -0,0 +1,48 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn from_ods_file_to_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.ods + | get SalesOrders + | get 4 + | get column2 + "# + )); + + assert_eq!(actual.out, "Gill"); +} + +#[test] +fn from_ods_file_to_table_select_sheet() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.ods --raw + | from ods --sheets ["SalesOrders"] + | columns + | get 0 + "# + )); + + assert_eq!(actual.out, "SalesOrders"); +} + +#[test] +fn from_ods_file_to_table_select_sheet_with_annotations() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data_with_annotation.ods --raw + | from ods --sheets ["SalesOrders"] + | get SalesOrders + | get column4 + | get 0 + "# + )); + + // The Units column in the sheet SalesOrders has an annotation and should be ignored. + assert_eq!(actual.out, "Units"); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/ssv.rs b/nushell/crates/nu-command/tests/format_conversions/ssv.rs new file mode 100644 index 0000000..5a62f79 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/ssv.rs @@ -0,0 +1,95 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn from_ssv_text_to_table() { + Playground::setup("filter_from_ssv_test_1", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "oc_get_svc.txt", + r#" + NAME LABELS SELECTOR IP PORT(S) + docker-registry docker-registry=default docker-registry=default 172.30.78.158 5000/TCP + kubernetes component=apiserver,provider=kubernetes 172.30.0.2 443/TCP + kubernetes-ro component=apiserver,provider=kubernetes 172.30.0.1 80/TCP + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open oc_get_svc.txt + | from ssv + | get 0 + | get IP + "# + )); + + assert_eq!(actual.out, "172.30.78.158"); + }) +} + +#[test] +fn from_ssv_text_to_table_with_separator_specified() { + Playground::setup("filter_from_ssv_test_1", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "oc_get_svc.txt", + r#" + NAME LABELS SELECTOR IP PORT(S) + docker-registry docker-registry=default docker-registry=default 172.30.78.158 5000/TCP + kubernetes component=apiserver,provider=kubernetes 172.30.0.2 443/TCP + kubernetes-ro component=apiserver,provider=kubernetes 172.30.0.1 80/TCP + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open oc_get_svc.txt + | from ssv --minimum-spaces 3 + | get 0 + | get IP + "# + )); + + assert_eq!(actual.out, "172.30.78.158"); + }) +} + +#[test] +fn from_ssv_text_treating_first_line_as_data_with_flag() { + Playground::setup("filter_from_ssv_test_2", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "oc_get_svc.txt", + r#" + docker-registry docker-registry=default docker-registry=default 172.30.78.158 5000/TCP + kubernetes component=apiserver,provider=kubernetes 172.30.0.2 443/TCP + kubernetes-ro component=apiserver,provider=kubernetes 172.30.0.1 80/TCP + "#, + )]); + + let aligned_columns = nu!( + cwd: dirs.test(), pipeline( + r#" + open oc_get_svc.txt + | from ssv --noheaders -a + | first + | get column0 + "# + )); + + let separator_based = nu!( + cwd: dirs.test(), pipeline( + r#" + open oc_get_svc.txt + | from ssv --noheaders + | first + | get column0 + + "# + )); + + assert_eq!(aligned_columns.out, separator_based.out); + assert_eq!(separator_based.out, "docker-registry"); + }) +} diff --git a/nushell/crates/nu-command/tests/format_conversions/toml.rs b/nushell/crates/nu-command/tests/format_conversions/toml.rs new file mode 100644 index 0000000..1b6e139 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/toml.rs @@ -0,0 +1,96 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn record_map_to_toml() { + let actual = nu!(pipeline( + r#" + {a: 1 b: 2 c: 'qwe'} + | to toml + | from toml + | $in == {a: 1 b: 2 c: 'qwe'} + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn nested_records_to_toml() { + let actual = nu!(pipeline( + r#" + {a: {a: a b: b} c: 1} + | to toml + | from toml + | $in == {a: {a: a b: b} c: 1} + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn records_with_tables_to_toml() { + let actual = nu!(pipeline( + r#" + {a: [[a b]; [1 2] [3 4]] b: [[c d e]; [1 2 3]]} + | to toml + | from toml + | $in == {a: [[a b]; [1 2] [3 4]] b: [[c d e]; [1 2 3]]} + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn nested_tables_to_toml() { + let actual = nu!(pipeline( + r#" + {c: [[f g]; [[[h k]; [1 2] [3 4]] 1]]} + | to toml + | from toml + | $in == {c: [[f g]; [[[h k]; [1 2] [3 4]] 1]]} + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn table_to_toml_fails() { + // Tables can't be represented in toml + let actual = nu!(pipeline( + r#" + try { [[a b]; [1 2] [5 6]] | to toml | false } catch { true } + "# + )); + + assert!(actual.err.contains("command doesn't support")); +} + +#[test] +fn string_to_toml_fails() { + // Strings are not a top-level toml structure + let actual = nu!(pipeline( + r#" + try { 'not a valid toml' | to toml | false } catch { true } + "# + )); + + assert!(actual.err.contains("command doesn't support")); +} + +#[test] +fn big_record_to_toml_text_and_from_toml_text_back_into_record() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml + | to toml + | from toml + | get package.name + "# + )); + + assert_eq!(actual.out, "nu"); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/tsv.rs b/nushell/crates/nu-command/tests/format_conversions/tsv.rs new file mode 100644 index 0000000..31740ef --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/tsv.rs @@ -0,0 +1,295 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn table_to_tsv_text_and_from_tsv_text_back_into_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "open caco3_plastics.tsv | to tsv | from tsv | first | get origin" + ); + + assert_eq!(actual.out, "SPAIN"); +} + +#[test] +fn table_to_tsv_text_and_from_tsv_text_back_into_table_using_csv_separator() { + let actual = nu!( + cwd: "tests/fixtures/formats", + r#"open caco3_plastics.tsv | to tsv | from csv --separator "\t" | first | get origin"# + ); + + assert_eq!(actual.out, "SPAIN"); +} + +#[test] +fn table_to_tsv_text() { + Playground::setup("filter_to_tsv_test_1", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "tsv_text_sample.txt", + r#" + importer shipper tariff_item name origin + Plasticos Rival Reverte 2509000000 Calcium carbonate Spain + Tigre Ecuador OMYA Andina 3824909999 Calcium carbonate Colombia + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open tsv_text_sample.txt + | lines + | split column "\t" a b c d origin + | last 1 + | to tsv + | lines + | select 1 + "# + )); + + assert!(actual.out.contains("Colombia")); + }) +} + +#[test] +fn table_to_tsv_text_skipping_headers_after_conversion() { + Playground::setup("filter_to_tsv_test_2", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "tsv_text_sample.txt", + r#" + importer shipper tariff_item name origin + Plasticos Rival Reverte 2509000000 Calcium carbonate Spain + Tigre Ecuador OMYA Andina 3824909999 Calcium carbonate Colombia + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open tsv_text_sample.txt + | lines + | split column "\t" a b c d origin + | last 1 + | to tsv --noheaders + "# + )); + + assert!(actual.out.contains("Colombia")); + }) +} + +#[test] +fn from_tsv_text_to_table() { + Playground::setup("filter_from_tsv_test_1", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "los_tres_amigos.txt", + r#" + first Name Last Name rusty_luck + Andrés Robalino 1 + JT Turner 1 + Yehuda Katz 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_amigos.txt + | from tsv + | get rusty_luck + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +#[ignore = "csv crate has a bug when the last line is a comment: https://github.com/BurntSushi/rust-csv/issues/363"] +fn from_tsv_text_with_comments_to_table() { + Playground::setup("filter_from_tsv_test_2", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + # This is a comment + first_name last_name rusty_luck + # This one too + Andrés Robalino 1 + Jonathan Turner 1 + Yehuda Katz 1 + # This one also + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r##" + open los_tres_caballeros.txt + | from tsv --comment "#" + | get rusty_luck + | length + "## + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn from_tsv_text_with_custom_quotes_to_table() { + Playground::setup("filter_from_tsv_test_3", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + first_name last_name rusty_luck + 'And''rés' Robalino 1 + Jonathan Turner 1 + Yehuda Katz 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | from tsv --quote "'" + | first + | get first_name + "# + )); + + assert_eq!(actual.out, "And'rés"); + }) +} + +#[test] +fn from_tsv_text_with_custom_escapes_to_table() { + Playground::setup("filter_from_tsv_test_4", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + first_name last_name rusty_luck + "And\"rés" Robalino 1 + Jonathan Turner 1 + Yehuda Katz 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r" + open los_tres_caballeros.txt + | from tsv --escape '\' + | first + | get first_name + " + )); + + assert_eq!(actual.out, "And\"rés"); + }) +} + +#[test] +fn from_tsv_text_skipping_headers_to_table() { + Playground::setup("filter_from_tsv_test_5", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "los_tres_amigos.txt", + r#" + Andrés Robalino 1 + JT Turner 1 + Yehuda Katz 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_amigos.txt + | from tsv --noheaders + | get column2 + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn from_tsv_text_with_missing_columns_to_table() { + Playground::setup("filter_from_tsv_test_6", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + first_name last_name rusty_luck + Andrés Robalino + Jonathan Turner 1 + Yehuda Katz 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | from tsv --flexible + | get -i rusty_luck + | compact + | length + "# + )); + + assert_eq!(actual.out, "2"); + }) +} + +#[test] +fn from_tsv_text_with_multiple_char_comment() { + Playground::setup("filter_from_tsv_test_7", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + first_name last_name rusty_luck + Andrés Robalino 1 + Jonathan Turner 1 + Yehuda Katz 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | from csv --comment "li" + "# + )); + + assert!(actual.err.contains("single character separator")); + }) +} + +#[test] +fn from_tsv_text_with_wrong_type_comment() { + Playground::setup("filter_from_csv_test_8", |dirs, sandbox| { + sandbox.with_files(&[FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + first_name last_name rusty_luck + Andrés Robalino 1 + Jonathan Turner 1 + Yehuda Katz 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | from csv --comment ('123' | into int) + "# + )); + + assert!(actual.err.contains("can't convert int to char")); + }) +} diff --git a/nushell/crates/nu-command/tests/format_conversions/url.rs b/nushell/crates/nu-command/tests/format_conversions/url.rs new file mode 100644 index 0000000..3660653 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/url.rs @@ -0,0 +1,16 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn can_encode_and_decode_urlencoding() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.url + | url build-query + | from url + | get cheese + "# + )); + + assert_eq!(actual.out, "comté"); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/xlsx.rs b/nushell/crates/nu-command/tests/format_conversions/xlsx.rs new file mode 100644 index 0000000..06c07e7 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/xlsx.rs @@ -0,0 +1,45 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn from_excel_file_to_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.xlsx + | get SalesOrders + | get 4 + | get column2 + "# + )); + + assert_eq!(actual.out, "Gill"); +} + +#[test] +fn from_excel_file_to_table_select_sheet() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.xlsx --raw + | from xlsx --sheets ["SalesOrders"] + | columns + | get 0 + "# + )); + + assert_eq!(actual.out, "SalesOrders"); +} + +#[test] +fn from_excel_file_to_date() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.xlsx + | get SalesOrders.4.column0 + | format date "%Y-%m-%d" + "# + )); + + assert_eq!(actual.out, "2018-02-26"); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/xml.rs b/nushell/crates/nu-command/tests/format_conversions/xml.rs new file mode 100644 index 0000000..cf84371 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/xml.rs @@ -0,0 +1,112 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn table_to_xml_text_and_from_xml_text_back_into_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open jt.xml + | to xml + | from xml + | get content + | where tag == channel + | get content + | flatten + | where tag == item + | get content + | flatten + | where tag == guid + | get 0.attributes.isPermaLink + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn to_xml_error_unknown_column() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + {tag: a bad_column: b} | to xml + "# + )); + + assert!(actual.err.contains("Invalid column \"bad_column\"")); +} + +#[test] +fn to_xml_error_no_tag() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + {attributes: {a: b c: d}} | to xml + "# + )); + + assert!(actual.err.contains("Tag missing")); +} + +#[test] +fn to_xml_error_tag_not_string() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + {tag: 1 attributes: {a: b c: d}} | to xml + "# + )); + + assert!(actual.err.contains("not a string")); +} + +#[test] +fn to_xml_partial_escape() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + { + tag: a + attributes: { a: "'a'\\" } + content: [ `'"qwe\` ] + } | to xml --partial-escape + "# + )); + assert_eq!(actual.out, r#"'"qwe\"#); +} + +#[test] +fn to_xml_pi_comment_not_escaped() { + // PI and comment content should not be escaped + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + { + tag: a + content: [ + {tag: ?qwe content: `"'<>&`} + {tag: ! content: `"'<>&`} + ] + } | to xml + "# + )); + assert_eq!(actual.out, r#"&?>"#); +} + +#[test] +fn to_xml_self_closed() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + { + tag: root + content: [ + [tag attributes content]; + [a null null] + [b {e: r} null] + [c {t: y} []] + ] + } | to xml --self-closed + "# + )); + assert_eq!(actual.out, r#""#); +} diff --git a/nushell/crates/nu-command/tests/format_conversions/yaml.rs b/nushell/crates/nu-command/tests/format_conversions/yaml.rs new file mode 100644 index 0000000..9aa9e16 --- /dev/null +++ b/nushell/crates/nu-command/tests/format_conversions/yaml.rs @@ -0,0 +1,81 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn table_to_yaml_text_and_from_yaml_text_back_into_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open appveyor.yml + | to yaml + | from yaml + | get environment.global.PROJECT_NAME + "# + )); + + assert_eq!(actual.out, "nushell"); +} + +#[test] +fn table_to_yml_text_and_from_yml_text_back_into_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open appveyor.yml + | to yml + | from yml + | get environment.global.PROJECT_NAME + "# + )); + + assert_eq!(actual.out, "nushell"); +} + +#[test] +fn convert_dict_to_yaml_with_boolean_key() { + let actual = nu!(pipeline( + r#" + "true: BooleanKey " | from yaml + "# + )); + assert!(actual.out.contains("BooleanKey")); + assert!(actual.err.is_empty()); +} + +#[test] +fn convert_dict_to_yaml_with_integer_key() { + let actual = nu!(pipeline( + r#" + "200: [] " | from yaml + "# + )); + + assert!(actual.out.contains("200")); + assert!(actual.err.is_empty()); +} + +#[test] +fn convert_dict_to_yaml_with_integer_floats_key() { + let actual = nu!(pipeline( + r#" + "2.11: "1" " | from yaml + "# + )); + assert!(actual.out.contains("2.11")); + assert!(actual.err.is_empty()); +} + +#[test] +#[ignore] +fn convert_bool_to_yaml_in_yaml_spec_1_2() { + let actual = nu!(pipeline( + r#" + [y n no On OFF True true false] | to yaml + "# + )); + + assert_eq!( + actual.out, + "- 'y'- 'n'- 'no'- 'On'- 'OFF'- 'True'- true- false" + ); + assert!(actual.err.is_empty()); +} diff --git a/nushell/crates/nu-command/tests/main.rs b/nushell/crates/nu-command/tests/main.rs new file mode 100644 index 0000000..f6f2ad6 --- /dev/null +++ b/nushell/crates/nu-command/tests/main.rs @@ -0,0 +1,4 @@ +mod commands; +mod format_conversions; +mod sort_utils; +mod string; diff --git a/nushell/crates/nu-command/tests/sort_utils.rs b/nushell/crates/nu-command/tests/sort_utils.rs new file mode 100644 index 0000000..c7043a2 --- /dev/null +++ b/nushell/crates/nu-command/tests/sort_utils.rs @@ -0,0 +1,559 @@ +use nu_command::{Comparator, sort, sort_by, sort_record}; +use nu_protocol::{ + Record, Span, Value, + ast::{CellPath, PathMember}, + casing::Casing, + record, +}; + +#[test] +fn test_sort_basic() { + let mut list = vec![ + Value::test_string("foo"), + Value::test_int(2), + Value::test_int(3), + Value::test_string("bar"), + Value::test_int(1), + Value::test_string("baz"), + ]; + + assert!(sort(&mut list, false, false).is_ok()); + assert_eq!( + list, + vec![ + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_string("bar"), + Value::test_string("baz"), + Value::test_string("foo") + ] + ); +} + +#[test] +fn test_sort_nothing() { + // Nothing values should always be sorted to the end of any list + let mut list = vec![ + Value::test_int(1), + Value::test_nothing(), + Value::test_int(2), + Value::test_string("foo"), + Value::test_nothing(), + Value::test_string("bar"), + ]; + + assert!(sort(&mut list, false, false).is_ok()); + assert_eq!( + list, + vec![ + Value::test_int(1), + Value::test_int(2), + Value::test_string("bar"), + Value::test_string("foo"), + Value::test_nothing(), + Value::test_nothing() + ] + ); + + // Ensure that nothing values are sorted after *all* types, + // even types which may follow `Nothing` in the PartialOrd order + + // unstable_name_collision + // can be switched to std intersperse when stabilized + let mut values: Vec = + itertools::intersperse(Value::test_values(), Value::test_nothing()).collect(); + + let nulls = values + .iter() + .filter(|item| item == &&Value::test_nothing()) + .count(); + + assert!(sort(&mut values, false, false).is_ok()); + + // check if the last `nulls` values of the sorted list are indeed null + assert_eq!(&values[(nulls - 1)..], vec![Value::test_nothing(); nulls]) +} + +#[test] +fn test_sort_natural_basic() { + let mut list = vec![ + Value::test_string("foo99"), + Value::test_string("foo9"), + Value::test_string("foo1"), + Value::test_string("foo100"), + Value::test_string("foo10"), + Value::test_string("1"), + Value::test_string("10"), + Value::test_string("100"), + Value::test_string("9"), + Value::test_string("99"), + ]; + + assert!(sort(&mut list, false, false).is_ok()); + assert_eq!( + list, + vec![ + Value::test_string("1"), + Value::test_string("10"), + Value::test_string("100"), + Value::test_string("9"), + Value::test_string("99"), + Value::test_string("foo1"), + Value::test_string("foo10"), + Value::test_string("foo100"), + Value::test_string("foo9"), + Value::test_string("foo99"), + ] + ); + + assert!(sort(&mut list, false, true).is_ok()); + assert_eq!( + list, + vec![ + Value::test_string("1"), + Value::test_string("9"), + Value::test_string("10"), + Value::test_string("99"), + Value::test_string("100"), + Value::test_string("foo1"), + Value::test_string("foo9"), + Value::test_string("foo10"), + Value::test_string("foo99"), + Value::test_string("foo100"), + ] + ); +} + +#[test] +fn test_sort_natural_mixed_types() { + let mut list = vec![ + Value::test_string("1"), + Value::test_int(99), + Value::test_int(1), + Value::test_float(1000.0), + Value::test_int(9), + Value::test_string("9"), + Value::test_int(100), + Value::test_string("99"), + Value::test_float(2.0), + Value::test_string("100"), + Value::test_int(10), + Value::test_string("10"), + ]; + + assert!(sort(&mut list, false, false).is_ok()); + assert_eq!( + list, + vec![ + Value::test_int(1), + Value::test_float(2.0), + Value::test_int(9), + Value::test_int(10), + Value::test_int(99), + Value::test_int(100), + Value::test_float(1000.0), + Value::test_string("1"), + Value::test_string("10"), + Value::test_string("100"), + Value::test_string("9"), + Value::test_string("99") + ] + ); + + assert!(sort(&mut list, false, true).is_ok()); + assert_eq!( + list, + vec![ + Value::test_int(1), + Value::test_string("1"), + Value::test_float(2.0), + Value::test_int(9), + Value::test_string("9"), + Value::test_int(10), + Value::test_string("10"), + Value::test_int(99), + Value::test_string("99"), + Value::test_int(100), + Value::test_string("100"), + Value::test_float(1000.0), + ] + ); +} + +#[test] +fn test_sort_natural_no_numeric_values() { + // If list contains no numeric strings, it should be sorted the + // same with or without natural sorting + let mut normal = vec![ + Value::test_string("golf"), + Value::test_bool(false), + Value::test_string("alfa"), + Value::test_string("echo"), + Value::test_int(7), + Value::test_int(10), + Value::test_bool(true), + Value::test_string("uniform"), + Value::test_int(3), + Value::test_string("tango"), + ]; + let mut natural = normal.clone(); + + assert!(sort(&mut normal, false, false).is_ok()); + assert!(sort(&mut natural, false, true).is_ok()); + assert_eq!(normal, natural); +} + +#[test] +fn test_sort_natural_type_order() { + // This test is to prevent regression to a previous natural sort behavior + // where values of different types would be intermixed. + // Only numeric values (ints, floats, and numeric strings) should be intermixed + // + // This list would previously be incorrectly sorted like this: + // ╭────┬─────────╮ + // │ 0 │ 1 │ + // │ 1 │ golf │ + // │ 2 │ false │ + // │ 3 │ 7 │ + // │ 4 │ 10 │ + // │ 5 │ alfa │ + // │ 6 │ true │ + // │ 7 │ uniform │ + // │ 8 │ true │ + // │ 9 │ 3 │ + // │ 10 │ false │ + // │ 11 │ tango │ + // ╰────┴─────────╯ + + let mut list = vec![ + Value::test_string("golf"), + Value::test_int(1), + Value::test_bool(false), + Value::test_string("alfa"), + Value::test_int(7), + Value::test_int(10), + Value::test_bool(true), + Value::test_string("uniform"), + Value::test_bool(true), + Value::test_int(3), + Value::test_bool(false), + Value::test_string("tango"), + ]; + + assert!(sort(&mut list, false, true).is_ok()); + assert_eq!( + list, + vec![ + Value::test_bool(false), + Value::test_bool(false), + Value::test_bool(true), + Value::test_bool(true), + Value::test_int(1), + Value::test_int(3), + Value::test_int(7), + Value::test_int(10), + Value::test_string("alfa"), + Value::test_string("golf"), + Value::test_string("tango"), + Value::test_string("uniform") + ] + ); + + // Only ints, floats, and numeric strings should be intermixed + // While binary primitives and datetimes can be coerced into strings, it doesn't make sense to sort them with numbers + // Binary primitives can hold multiple values, not just one, so shouldn't be compared to single values + // Datetimes don't have a single obvious numeric representation, and if we chose one it would be ambiguous to the user + + let year_three = chrono::NaiveDate::from_ymd_opt(3, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap() + .and_utc(); + + let mut list = vec![ + Value::test_int(10), + Value::test_float(6.0), + Value::test_int(1), + Value::test_binary([3]), + Value::test_string("2"), + Value::test_date(year_three.into()), + Value::test_int(4), + Value::test_binary([52]), + Value::test_float(9.0), + Value::test_string("5"), + Value::test_date(chrono::DateTime::UNIX_EPOCH.into()), + Value::test_int(7), + Value::test_string("8"), + Value::test_float(3.0), + Value::test_string("foobar"), + ]; + assert!(sort(&mut list, false, true).is_ok()); + assert_eq!( + list, + vec![ + Value::test_int(1), + Value::test_string("2"), + Value::test_float(3.0), + Value::test_int(4), + Value::test_string("5"), + Value::test_float(6.0), + Value::test_int(7), + Value::test_string("8"), + Value::test_float(9.0), + Value::test_int(10), + Value::test_string("foobar"), + // the ordering of date and binary here may change if the PartialOrd order is changed, + // but they should not be intermixed with the above + Value::test_date(year_three.into()), + Value::test_date(chrono::DateTime::UNIX_EPOCH.into()), + Value::test_binary([3]), + Value::test_binary([52]), + ] + ); +} + +#[test] +fn test_sort_insensitive() { + // Test permutations between insensitive and natural + // Ensure that strings with equal insensitive orderings + // are sorted stably. (FOO then foo, bar then BAR) + let source = vec![ + Value::test_string("FOO"), + Value::test_string("foo"), + Value::test_int(100), + Value::test_string("9"), + Value::test_string("bar"), + Value::test_int(10), + Value::test_string("baz"), + Value::test_string("BAR"), + ]; + let mut list; + + // sensitive + non-natural + list = source.clone(); + assert!(sort(&mut list, false, false).is_ok()); + assert_eq!( + list, + vec![ + Value::test_int(10), + Value::test_int(100), + Value::test_string("9"), + Value::test_string("BAR"), + Value::test_string("FOO"), + Value::test_string("bar"), + Value::test_string("baz"), + Value::test_string("foo"), + ] + ); + + // sensitive + natural + list = source.clone(); + assert!(sort(&mut list, false, true).is_ok()); + assert_eq!( + list, + vec![ + Value::test_string("9"), + Value::test_int(10), + Value::test_int(100), + Value::test_string("BAR"), + Value::test_string("FOO"), + Value::test_string("bar"), + Value::test_string("baz"), + Value::test_string("foo"), + ] + ); + + // insensitive + non-natural + list = source.clone(); + assert!(sort(&mut list, true, false).is_ok()); + assert_eq!( + list, + vec![ + Value::test_int(10), + Value::test_int(100), + Value::test_string("9"), + Value::test_string("bar"), + Value::test_string("BAR"), + Value::test_string("baz"), + Value::test_string("FOO"), + Value::test_string("foo"), + ] + ); + + // insensitive + natural + list = source.clone(); + assert!(sort(&mut list, true, true).is_ok()); + assert_eq!( + list, + vec![ + Value::test_string("9"), + Value::test_int(10), + Value::test_int(100), + Value::test_string("bar"), + Value::test_string("BAR"), + Value::test_string("baz"), + Value::test_string("FOO"), + Value::test_string("foo"), + ] + ); +} + +// Helper function to assert that two records are equal +// with their key-value pairs in the same order +fn assert_record_eq(a: Record, b: Record) { + assert_eq!( + a.into_iter().collect::>(), + b.into_iter().collect::>(), + ) +} + +#[test] +fn test_sort_record_keys() { + // Basic record sort test + let record = record! { + "golf" => Value::test_string("bar"), + "alfa" => Value::test_string("foo"), + "echo" => Value::test_int(123), + }; + + let sorted = sort_record(record, false, false, false, false).unwrap(); + assert_record_eq( + sorted, + record! { + "alfa" => Value::test_string("foo"), + "echo" => Value::test_int(123), + "golf" => Value::test_string("bar"), + }, + ); +} + +#[test] +fn test_sort_record_values() { + // This test is to prevent a regression where integers and strings would be + // intermixed non-naturally when sorting a record by value without the natural flag: + // + // This record would previously be incorrectly sorted like this: + // ╭─────────┬─────╮ + // │ alfa │ 1 │ + // │ charlie │ 1 │ + // │ india │ 10 │ + // │ juliett │ 10 │ + // │ foxtrot │ 100 │ + // │ hotel │ 100 │ + // │ delta │ 9 │ + // │ echo │ 9 │ + // │ bravo │ 99 │ + // │ golf │ 99 │ + // ╰─────────┴─────╯ + + let record = record! { + "alfa" => Value::test_string("1"), + "bravo" => Value::test_int(99), + "charlie" => Value::test_int(1), + "delta" => Value::test_int(9), + "echo" => Value::test_string("9"), + "foxtrot" => Value::test_int(100), + "golf" => Value::test_string("99"), + "hotel" => Value::test_string("100"), + "india" => Value::test_int(10), + "juliett" => Value::test_string("10"), + }; + + // non-natural sort + let sorted = sort_record(record.clone(), true, false, false, false).unwrap(); + assert_record_eq( + sorted, + record! { + "charlie" => Value::test_int(1), + "delta" => Value::test_int(9), + "india" => Value::test_int(10), + "bravo" => Value::test_int(99), + "foxtrot" => Value::test_int(100), + "alfa" => Value::test_string("1"), + "juliett" => Value::test_string("10"), + "hotel" => Value::test_string("100"), + "echo" => Value::test_string("9"), + "golf" => Value::test_string("99"), + }, + ); + + // natural sort + let sorted = sort_record(record.clone(), true, false, false, true).unwrap(); + assert_record_eq( + sorted, + record! { + "alfa" => Value::test_string("1"), + "charlie" => Value::test_int(1), + "delta" => Value::test_int(9), + "echo" => Value::test_string("9"), + "india" => Value::test_int(10), + "juliett" => Value::test_string("10"), + "bravo" => Value::test_int(99), + "golf" => Value::test_string("99"), + "foxtrot" => Value::test_int(100), + "hotel" => Value::test_string("100"), + }, + ); +} + +#[test] +fn test_sort_equivalent() { + // Ensure that sort, sort_by, and record sort have equivalent sorting logic + let phonetic = vec![ + "alfa", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", + "juliett", "kilo", "lima", "mike", "november", "oscar", "papa", "quebec", "romeo", + "sierra", "tango", "uniform", "victor", "whiskey", "xray", "yankee", "zulu", + ]; + + // filter out errors, since we can't sort_by on those + let mut values: Vec = Value::test_values() + .into_iter() + .filter(|val| !matches!(val, Value::Error { .. })) + .collect(); + + // reverse sort test values + values.sort_by(|a, b| b.partial_cmp(a).unwrap()); + + let mut list = values.clone(); + let mut table: Vec = values + .clone() + .into_iter() + .map(|val| Value::test_record(record! { "value" => val })) + .collect(); + let record = Record::from_iter(phonetic.into_iter().map(str::to_string).zip(values)); + + let comparator = Comparator::CellPath(CellPath { + members: vec![PathMember::String { + val: "value".to_string(), + span: Span::test_data(), + optional: false, + casing: Casing::Sensitive, + }], + }); + + assert!(sort(&mut list, false, false).is_ok()); + assert!( + sort_by( + &mut table, + vec![comparator], + Span::test_data(), + false, + false + ) + .is_ok() + ); + + let record_sorted = sort_record(record.clone(), true, false, false, false).unwrap(); + let record_vals: Vec = record_sorted.into_iter().map(|pair| pair.1).collect(); + + let table_vals: Vec = table + .clone() + .into_iter() + .map(|record| record.into_record().unwrap().remove("value").unwrap()) + .collect(); + + assert_eq!(list, record_vals); + assert_eq!(record_vals, table_vals); + // list == table_vals by transitive property +} diff --git a/nushell/crates/nu-command/tests/string/format/duration.rs b/nushell/crates/nu-command/tests/string/format/duration.rs new file mode 100644 index 0000000..d5fb702 --- /dev/null +++ b/nushell/crates/nu-command/tests/string/format/duration.rs @@ -0,0 +1,15 @@ +use nu_test_support::nu; + +#[test] +fn format_duration() { + let actual = nu!(r#"1hr | format duration sec"#); + + assert_eq!("3600 sec", actual.out); +} + +#[test] +fn format_duration_with_invalid_unit() { + let actual = nu!(r#"1hr | format duration MB"#); + + assert!(actual.err.contains("invalid_unit")); +} diff --git a/nushell/crates/nu-command/tests/string/format/filesize.rs b/nushell/crates/nu-command/tests/string/format/filesize.rs new file mode 100644 index 0000000..426c6bd --- /dev/null +++ b/nushell/crates/nu-command/tests/string/format/filesize.rs @@ -0,0 +1,15 @@ +use nu_test_support::nu; + +#[test] +fn format_duration() { + let actual = nu!(r#"1MB | format filesize kB"#); + + assert_eq!("1000 kB", actual.out); +} + +#[test] +fn format_duration_with_invalid_unit() { + let actual = nu!(r#"1MB | format filesize sec"#); + + assert!(actual.err.contains("invalid_unit")); +} diff --git a/nushell/crates/nu-command/tests/string/format/mod.rs b/nushell/crates/nu-command/tests/string/format/mod.rs new file mode 100644 index 0000000..d1a100f --- /dev/null +++ b/nushell/crates/nu-command/tests/string/format/mod.rs @@ -0,0 +1,2 @@ +mod duration; +mod filesize; diff --git a/nushell/crates/nu-command/tests/string/mod.rs b/nushell/crates/nu-command/tests/string/mod.rs new file mode 100644 index 0000000..8631268 --- /dev/null +++ b/nushell/crates/nu-command/tests/string/mod.rs @@ -0,0 +1 @@ +mod format; diff --git a/nushell/crates/nu-derive-value/Cargo.toml b/nushell/crates/nu-derive-value/Cargo.toml new file mode 100644 index 0000000..66724aa --- /dev/null +++ b/nushell/crates/nu-derive-value/Cargo.toml @@ -0,0 +1,24 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Macros implementation of #[derive(FromValue, IntoValue)]" +edition = "2024" +license = "MIT" +name = "nu-derive-value" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value" +version = "0.105.2" + +[lib] +proc-macro = true +# we can only use exposed macros in doctests really, +# so we cannot test anything useful in a doctest +doctest = false + +[lints] +workspace = true + +[dependencies] +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } +proc-macro-error2 = { workspace = true } +heck = { workspace = true } diff --git a/nushell/crates/nu-derive-value/LICENSE b/nushell/crates/nu-derive-value/LICENSE new file mode 100644 index 0000000..ae174e8 --- /dev/null +++ b/nushell/crates/nu-derive-value/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nushell/crates/nu-derive-value/src/attributes.rs b/nushell/crates/nu-derive-value/src/attributes.rs new file mode 100644 index 0000000..6718b01 --- /dev/null +++ b/nushell/crates/nu-derive-value/src/attributes.rs @@ -0,0 +1,133 @@ +use syn::{Attribute, Fields, LitStr, meta::ParseNestedMeta, spanned::Spanned}; + +use crate::{HELPER_ATTRIBUTE, case::Case, error::DeriveError}; + +pub trait ParseAttrs: Default { + fn parse_attrs<'a, M>( + iter: impl IntoIterator, + ) -> Result> { + let mut attrs = Self::default(); + for attr in filter(iter.into_iter()) { + // This is a container to allow returning derive errors inside the parse_nested_meta fn. + let mut err = Ok(()); + let _ = attr.parse_nested_meta(|meta| { + attrs.parse_attr(meta).or_else(|e| { + err = Err(e); + Ok(()) // parse_nested_meta requires another error type, so we escape it here + }) + }); + err?; // Shortcircuit here if `err` is holding some error. + } + + Ok(attrs) + } + + fn parse_attr(&mut self, attr_meta: ParseNestedMeta<'_>) -> Result<(), DeriveError>; +} + +#[derive(Debug, Default)] +pub struct ContainerAttributes { + pub rename_all: Option, + pub type_name: Option, +} + +impl ParseAttrs for ContainerAttributes { + fn parse_attr(&mut self, attr_meta: ParseNestedMeta<'_>) -> Result<(), DeriveError> { + let ident = attr_meta.path.require_ident()?; + match ident.to_string().as_str() { + "rename_all" => { + let case: LitStr = attr_meta.value()?.parse()?; + let value_span = case.span(); + let case = case.value(); + match Case::from_str(&case) { + Some(case) => self.rename_all = Some(case), + None => { + return Err(DeriveError::InvalidAttributeValue { + value_span, + value: Box::new(case), + }); + } + } + } + "type_name" => { + let type_name: LitStr = attr_meta.value()?.parse()?; + let type_name = type_name.value(); + self.type_name = Some(type_name); + } + ident => { + return Err(DeriveError::UnexpectedAttribute { + meta_span: ident.span(), + }); + } + } + + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct MemberAttributes { + pub rename: Option, + pub default: bool, +} + +impl ParseAttrs for MemberAttributes { + fn parse_attr(&mut self, attr_meta: ParseNestedMeta<'_>) -> Result<(), DeriveError> { + let ident = attr_meta.path.require_ident()?; + match ident.to_string().as_str() { + "rename" => { + let rename: LitStr = attr_meta.value()?.parse()?; + let rename = rename.value(); + self.rename = Some(rename); + } + "default" => { + self.default = true; + } + ident => { + return Err(DeriveError::UnexpectedAttribute { + meta_span: ident.span(), + }); + } + } + + Ok(()) + } +} + +pub fn filter<'a>( + iter: impl Iterator, +) -> impl Iterator { + iter.filter(|attr| attr.path().is_ident(HELPER_ATTRIBUTE)) +} + +// The deny functions are built to easily deny the use of the helper attribute if used incorrectly. +// As the usage of it gets more complex, these functions might be discarded or replaced. + +/// Deny any attribute that uses the helper attribute. +pub fn deny(attrs: &[Attribute]) -> Result<(), DeriveError> { + match filter(attrs.iter()).next() { + Some(attr) => Err(DeriveError::InvalidAttributePosition { + attribute_span: attr.span(), + }), + None => Ok(()), + } +} + +/// Deny any attributes that uses the helper attribute on any field. +pub fn deny_fields(fields: &Fields) -> Result<(), DeriveError> { + match fields { + Fields::Named(fields) => { + for field in fields.named.iter() { + deny(&field.attrs)?; + } + } + Fields::Unnamed(fields) => { + for field in fields.unnamed.iter() { + deny(&field.attrs)?; + } + } + Fields::Unit => (), + } + + Ok(()) +} diff --git a/nushell/crates/nu-derive-value/src/case.rs b/nushell/crates/nu-derive-value/src/case.rs new file mode 100644 index 0000000..5962a29 --- /dev/null +++ b/nushell/crates/nu-derive-value/src/case.rs @@ -0,0 +1,76 @@ +use heck::*; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Case { + // directly supported by heck + Pascal, + Camel, + Snake, + Kebab, + ScreamingSnake, + Title, + Cobol, + Train, + + // custom variants + Upper, + Lower, + Flat, + ScreamingFlat, +} + +impl Case { + pub fn from_str(s: impl AsRef) -> Option { + match s.as_ref() { + // The matched case are all useful variants from `convert_case` with aliases + // that `serde` uses. + "PascalCase" | "UpperCamelCase" => Case::Pascal, + "camelCase" | "lowerCamelCase" => Case::Camel, + "snake_case" => Case::Snake, + "kebab-case" => Case::Kebab, + "SCREAMING_SNAKE_CASE" | "UPPER_SNAKE_CASE" | "SHOUTY_SNAKE_CASE" => { + Case::ScreamingSnake + } + "Title Case" => Case::Title, + "COBOL-CASE" | "SCREAMING-KEBAB-CASE" | "UPPER-KEBAB-CASE" => Case::Cobol, + "Train-Case" => Case::Train, + + "UPPER CASE" | "UPPER WITH SPACES CASE" => Case::Upper, + "lower case" | "lower with spaces case" => Case::Lower, + "flatcase" | "lowercase" => Case::Flat, + "SCREAMINGFLATCASE" | "UPPERFLATCASE" | "UPPERCASE" => Case::ScreamingFlat, + + _ => return None, + } + .into() + } +} + +pub trait Casing { + fn to_case(&self, case: impl Into>) -> String; +} + +impl Casing for T { + fn to_case(&self, case: impl Into>) -> String { + let s = self.to_string(); + let Some(case) = case.into() else { + return s.to_string(); + }; + + match case { + Case::Pascal => s.to_upper_camel_case(), + Case::Camel => s.to_lower_camel_case(), + Case::Snake => s.to_snake_case(), + Case::Kebab => s.to_kebab_case(), + Case::ScreamingSnake => s.to_shouty_snake_case(), + Case::Title => s.to_title_case(), + Case::Cobol => s.to_shouty_kebab_case(), + Case::Train => s.to_train_case(), + + Case::Upper => s.to_shouty_snake_case().replace('_', " "), + Case::Lower => s.to_snake_case().replace('_', " "), + Case::Flat => s.to_snake_case().replace('_', ""), + Case::ScreamingFlat => s.to_shouty_snake_case().replace('_', ""), + } + } +} diff --git a/nushell/crates/nu-derive-value/src/error.rs b/nushell/crates/nu-derive-value/src/error.rs new file mode 100644 index 0000000..eb9bd28 --- /dev/null +++ b/nushell/crates/nu-derive-value/src/error.rs @@ -0,0 +1,104 @@ +use std::{any, fmt::Debug, marker::PhantomData}; + +use proc_macro_error2::{Diagnostic, Level}; +use proc_macro2::Span; + +#[derive(Debug)] +pub enum DeriveError { + /// Marker variant, makes the `M` generic parameter valid. + _Marker(PhantomData), + + /// Parsing errors thrown by `syn`. + Syn(syn::parse::Error), + + /// `syn::DeriveInput` was a union, currently not supported + UnsupportedUnions, + + /// Only plain enums are supported right now. + UnsupportedEnums { fields_span: Span }, + + /// Found a `#[nu_value(x)]` attribute where `x` is unexpected. + UnexpectedAttribute { meta_span: Span }, + + /// Found a `#[nu_value(x)]` attribute at a invalid position. + InvalidAttributePosition { attribute_span: Span }, + + /// Found a valid `#[nu_value(x)]` attribute but the passed values is invalid. + InvalidAttributeValue { + value_span: Span, + value: Box, + }, + + /// Two keys or variants are called the same name breaking bidirectionality. + NonUniqueName { + name: String, + first: Span, + second: Span, + }, +} + +impl From for DeriveError { + fn from(value: syn::parse::Error) -> Self { + Self::Syn(value) + } +} + +impl From> for Diagnostic { + fn from(value: DeriveError) -> Self { + let derive_name = any::type_name::().split("::").last().expect("not empty"); + match value { + DeriveError::_Marker(_) => panic!("used marker variant"), + + DeriveError::Syn(e) => Diagnostic::spanned(e.span(), Level::Error, e.to_string()), + + DeriveError::UnsupportedUnions => Diagnostic::new( + Level::Error, + format!("`{derive_name}` cannot be derived from unions"), + ) + .help("consider refactoring to a struct".to_string()) + .note("if you really need a union, consider opening an issue on Github".to_string()), + + DeriveError::UnsupportedEnums { fields_span } => Diagnostic::spanned( + fields_span, + Level::Error, + format!("`{derive_name}` can only be derived from plain enums"), + ) + .help( + "consider refactoring your data type to a struct with a plain enum as a field" + .to_string(), + ) + .note("more complex enums could be implemented in the future".to_string()), + + DeriveError::InvalidAttributePosition { attribute_span } => Diagnostic::spanned( + attribute_span, + Level::Error, + "invalid attribute position".to_string(), + ) + .help(format!( + "check documentation for `{derive_name}` for valid placements" + )), + + DeriveError::UnexpectedAttribute { meta_span } => { + Diagnostic::spanned(meta_span, Level::Error, "unknown attribute".to_string()).help( + format!("check documentation for `{derive_name}` for valid attributes"), + ) + } + + DeriveError::InvalidAttributeValue { value_span, value } => { + Diagnostic::spanned(value_span, Level::Error, format!("invalid value {value:?}")) + .help(format!( + "check documentation for `{derive_name}` for valid attribute values" + )) + } + + DeriveError::NonUniqueName { + name, + first, + second, + } => Diagnostic::new(Level::Error, format!("non-unique name {name:?} found")) + .span_error(first, "first occurrence found here".to_string()) + .span_error(second, "second occurrence found here".to_string()) + .help("use `#[nu_value(rename = \"...\")]` to ensure unique names".to_string()), + } + } +} diff --git a/nushell/crates/nu-derive-value/src/from.rs b/nushell/crates/nu-derive-value/src/from.rs new file mode 100644 index 0000000..41ad461 --- /dev/null +++ b/nushell/crates/nu-derive-value/src/from.rs @@ -0,0 +1,664 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, quote}; +use syn::{ + Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident, Type, + spanned::Spanned, +}; + +use crate::{ + attributes::{self, ContainerAttributes, MemberAttributes, ParseAttrs}, + case::Case, + names::NameResolver, +}; + +#[derive(Debug)] +pub struct FromValue; +type DeriveError = super::error::DeriveError; +type Result = std::result::Result; + +/// Inner implementation of the `#[derive(FromValue)]` macro for structs and enums. +/// +/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`. +/// +/// This function directs the `FromValue` trait derivation to the correct implementation based on +/// the input type: +/// - For structs: [`derive_struct_from_value`] +/// - For enums: [`derive_enum_from_value`] +/// - Unions are not supported and will return an error. +pub fn derive_from_value(input: TokenStream2) -> Result { + let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?; + match input.data { + Data::Struct(data_struct) => Ok(derive_struct_from_value( + input.ident, + data_struct, + input.generics, + input.attrs, + )?), + Data::Enum(data_enum) => Ok(derive_enum_from_value( + input.ident, + data_enum, + input.generics, + input.attrs, + )?), + Data::Union(_) => Err(DeriveError::UnsupportedUnions), + } +} + +/// Implements the `#[derive(FromValue)]` macro for structs. +/// +/// This function provides the impl signature for `FromValue`. +/// The implementation for `FromValue::from_value` is handled by [`struct_from_value`] and the +/// `FromValue::expected_type` is handled by [`struct_expected_type`]. +fn derive_struct_from_value( + ident: Ident, + data: DataStruct, + generics: Generics, + attrs: Vec, +) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let from_value_impl = struct_from_value(&data, &container_attrs)?; + let expected_type_impl = struct_expected_type( + &data.fields, + container_attrs.type_name.as_deref(), + &container_attrs, + )?; + Ok(quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause { + #from_value_impl + #expected_type_impl + } + }) +} + +/// Implements `FromValue::from_value` for structs. +/// +/// This function constructs the `from_value` function for structs. +/// The implementation is straightforward as most of the heavy lifting is handled by +/// [`parse_value_via_fields`], and this function only needs to construct the signature around it. +/// +/// For structs with named fields, this constructs a large return type where each field +/// contains the implementation for that specific field. +/// In structs with unnamed fields, a [`VecDeque`](std::collections::VecDeque) is used to load each +/// field one after another, and the result is used to construct the tuple. +/// For unit structs, this only checks if the input value is `Value::Nothing`. +/// +/// # Examples +/// +/// These examples show what the macro would generate. +/// +/// Struct with named fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Pet { +/// name: String, +/// age: u8, +/// favorite_toy: Option, +/// } +/// +/// impl nu_protocol::FromValue for Pet { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// let span = v.span(); +/// let mut record = v.into_record()?; +/// std::result::Result::Ok(Pet { +/// name: ::from_value( +/// record +/// .remove("name") +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string("name"), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )?, +/// age: ::from_value( +/// record +/// .remove("age") +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string("age"), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )?, +/// favorite_toy: record +/// .remove("favorite_toy") +/// .map(|v| <#ty as nu_protocol::FromValue>::from_value(v)) +/// .transpose()? +/// .flatten(), +/// }) +/// } +/// } +/// ``` +/// +/// Struct with unnamed fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Color(u8, u8, u8); +/// +/// impl nu_protocol::FromValue for Color { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// let span = v.span(); +/// let list = v.into_list()?; +/// let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list); +/// std::result::Result::Ok(Self( +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&0), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// }, +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&1), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// }, +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&2), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// } +/// )) +/// } +/// } +/// ``` +/// +/// Unit struct: +/// ```rust +/// #[derive(IntoValue)] +/// struct Unicorn; +/// +/// impl nu_protocol::FromValue for Unicorn { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// match v { +/// nu_protocol::Value::Nothing {..} => Ok(Self), +/// v => std::result::Result::Err(nu_protocol::ShellError::CantConvert { +/// to_type: std::string::ToString::to_string(&::expected_type()), +/// from_type: std::string::ToString::to_string(&v.get_type()), +/// span: v.span(), +/// help: std::option::Option::None +/// }) +/// } +/// } +/// } +/// ``` +fn struct_from_value(data: &DataStruct, container_attrs: &ContainerAttributes) -> Result { + let body = parse_value_via_fields(&data.fields, quote!(Self), container_attrs)?; + Ok(quote! { + fn from_value( + v: nu_protocol::Value + ) -> std::result::Result { + #body + } + }) +} + +/// Implements `FromValue::expected_type` for structs. +/// +/// This function constructs the `expected_type` function for structs based on the provided fields. +/// The type depends on the `fields`: +/// - Named fields construct a record type where each key corresponds to a field name. +/// The specific keys are resolved by [`NameResolver::resolve_ident`]. +/// - Unnamed fields construct a custom type with the format `list[type0, type1, type2]`. +/// - Unit structs expect `Type::Nothing`. +/// +/// If the `#[nu_value(type_name = "...")]` attribute is used, the output type will be +/// `Type::Custom` with the provided name. +/// +/// # Examples +/// +/// These examples show what the macro would generate. +/// +/// Struct with named fields: +/// ```rust +/// #[derive(FromValue)] +/// struct Pet { +/// name: String, +/// age: u8, +/// #[nu_value(rename = "toy")] +/// favorite_toy: Option, +/// } +/// +/// impl nu_protocol::FromValue for Pet { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Record( +/// std::vec![ +/// ( +/// std::string::ToString::to_string("name"), +/// ::expected_type(), +/// ), +/// ( +/// std::string::ToString::to_string("age"), +/// ::expected_type(), +/// ), +/// ( +/// std::string::ToString::to_string("toy"), +/// as nu_protocol::FromValue>::expected_type(), +/// ) +/// ].into_boxed_slice() +/// ) +/// } +/// } +/// ``` +/// +/// Struct with unnamed fields: +/// ```rust +/// #[derive(FromValue)] +/// struct Color(u8, u8, u8); +/// +/// impl nu_protocol::FromValue for Color { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Custom( +/// std::format!( +/// "[{}, {}, {}]", +/// ::expected_type(), +/// ::expected_type(), +/// ::expected_type() +/// ) +/// .into_boxed_str() +/// ) +/// } +/// } +/// ``` +/// +/// Unit struct: +/// ```rust +/// #[derive(FromValue)] +/// struct Unicorn; +/// +/// impl nu_protocol::FromValue for Color { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Nothing +/// } +/// } +/// ``` +/// +/// Struct with passed type name: +/// ```rust +/// #[derive(FromValue)] +/// #[nu_value(type_name = "bird")] +/// struct Parrot; +/// +/// impl nu_protocol::FromValue for Parrot { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Custom( +/// >::from("bird") +/// .into_boxed_str() +/// ) +/// } +/// } +/// ``` +fn struct_expected_type( + fields: &Fields, + attr_type_name: Option<&str>, + container_attrs: &ContainerAttributes, +) -> Result { + let ty = match (fields, attr_type_name) { + (_, Some(type_name)) => { + quote!(nu_protocol::Type::Custom( + >::from(#type_name).into_boxed_str() + )) + } + (Fields::Named(fields), _) => { + let mut name_resolver = NameResolver::new(); + let mut fields_ts = Vec::with_capacity(fields.named.len()); + for field in fields.named.iter() { + let member_attrs = MemberAttributes::parse_attrs(&field.attrs)?; + let ident = field.ident.as_ref().expect("named has idents"); + let ident_s = + name_resolver.resolve_ident(ident, container_attrs, &member_attrs, None)?; + let ty = &field.ty; + fields_ts.push(quote! {( + std::string::ToString::to_string(#ident_s), + <#ty as nu_protocol::FromValue>::expected_type(), + )}); + } + quote!(nu_protocol::Type::Record( + std::vec![#(#fields_ts),*].into_boxed_slice() + )) + } + (f @ Fields::Unnamed(fields), _) => { + attributes::deny_fields(f)?; + let mut iter = fields.unnamed.iter(); + let fields = fields.unnamed.iter().map(|field| { + let ty = &field.ty; + quote!(<#ty as nu_protocol::FromValue>::expected_type()) + }); + let mut template = String::new(); + template.push('['); + if iter.next().is_some() { + template.push_str("{}") + } + iter.for_each(|_| template.push_str(", {}")); + template.push(']'); + quote! { + nu_protocol::Type::Custom( + std::format!( + #template, + #(#fields),* + ) + .into_boxed_str() + ) + } + } + (Fields::Unit, _) => quote!(nu_protocol::Type::Nothing), + }; + + Ok(quote! { + fn expected_type() -> nu_protocol::Type { + #ty + } + }) +} + +/// Implements the `#[derive(FromValue)]` macro for enums. +/// +/// This function constructs the implementation of the `FromValue` trait for enums. +/// It is designed to be on the same level as [`derive_struct_from_value`], even though this +/// implementation is a lot simpler. +/// The main `FromValue::from_value` implementation is handled by [`enum_from_value`]. +/// The `FromValue::expected_type` implementation is usually kept empty to use the default +/// implementation, but if `#[nu_value(type_name = "...")]` if given, we use that. +fn derive_enum_from_value( + ident: Ident, + data: DataEnum, + generics: Generics, + attrs: Vec, +) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let from_value_impl = enum_from_value(&data, &attrs)?; + let expected_type_impl = enum_expected_type(container_attrs.type_name.as_deref()); + Ok(quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause { + #from_value_impl + #expected_type_impl + } + }) +} + +/// Implements `FromValue::from_value` for enums. +/// +/// This function constructs the `from_value` implementation for enums. +/// It only accepts enums with unit variants, as it is currently unclear how other types of enums +/// should be represented via a `Value`. +/// This function checks that every field is a unit variant and constructs a match statement over +/// all possible variants. +/// The input value is expected to be a `Value::String` containing the name of the variant. +/// That string is defined by the [`NameResolver::resolve_ident`] method with the `default` value +/// being [`Case::Snake`]. +/// +/// If no matching variant is found, `ShellError::CantConvert` is returned. +/// +/// This is how such a derived implementation looks: +/// ```rust +/// #[derive(IntoValue)] +/// enum Weather { +/// Sunny, +/// Cloudy, +/// #[nu_value(rename = "rain")] +/// Raining +/// } +/// +/// impl nu_protocol::IntoValue for Weather { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// let span = v.span(); +/// let ty = v.get_type(); +/// +/// let s = v.into_string()?; +/// match s.as_str() { +/// "sunny" => std::result::Ok(Self::Sunny), +/// "cloudy" => std::result::Ok(Self::Cloudy), +/// "rain" => std::result::Ok(Self::Raining), +/// _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert { +/// to_type: std::string::ToString::to_string( +/// &::expected_type() +/// ), +/// from_type: std::string::ToString::to_string(&ty), +/// span: span,help: std::option::Option::None, +/// }), +/// } +/// } +/// } +/// ``` +fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let mut name_resolver = NameResolver::new(); + let arms: Vec = data + .variants + .iter() + .map(|variant| { + let member_attrs = MemberAttributes::parse_attrs(&variant.attrs)?; + let ident = &variant.ident; + let ident_s = + name_resolver.resolve_ident(ident, &container_attrs, &member_attrs, Case::Snake)?; + match &variant.fields { + Fields::Named(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unit => Ok(quote!(#ident_s => std::result::Result::Ok(Self::#ident))), + } + }) + .collect::>()?; + + Ok(quote! { + fn from_value( + v: nu_protocol::Value + ) -> std::result::Result { + let span = v.span(); + let ty = v.get_type(); + + let s = v.into_string()?; + match s.as_str() { + #(#arms,)* + _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert { + to_type: std::string::ToString::to_string( + &::expected_type() + ), + from_type: std::string::ToString::to_string(&ty), + span: span, + help: std::option::Option::None, + }), + } + } + }) +} + +/// Implements `FromValue::expected_type` for enums. +/// +/// Since it's difficult to name the type of an enum in the current type system, we want to use the +/// default implementation if `#[nu_value(type_name = "...")]` was *not* given. +/// For that, a `None` value is returned, for a passed type name we return something like this: +/// ```rust +/// #[derive(IntoValue)] +/// #[nu_value(type_name = "sunny | cloudy | raining")] +/// enum Weather { +/// Sunny, +/// Cloudy, +/// Raining +/// } +/// +/// impl nu_protocol::FromValue for Weather { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Custom( +/// >::from("sunny | cloudy | raining") +/// .into_boxed_str() +/// ) +/// } +/// } +/// ``` +fn enum_expected_type(attr_type_name: Option<&str>) -> Option { + let type_name = attr_type_name?; + Some(quote! { + fn expected_type() -> nu_protocol::Type { + nu_protocol::Type::Custom( + >::from(#type_name) + .into_boxed_str() + ) + } + }) +} + +/// Parses a `Value` into self. +/// +/// This function handles parsing a `Value` into the corresponding struct or enum variant (`self`). +/// It takes three parameters: `fields`, `self_ident`, and `rename_all`. +/// +/// - The `fields` parameter specifies the expected structure of the `Value`: +/// - Named fields expect a `Value::Record`. +/// - Unnamed fields expect a `Value::List`. +/// - A unit struct expects `Value::Nothing`. +/// +/// For named fields, each field in the record is matched to a struct field. +/// The name matching uses the identifiers resolved by +/// [`NameResolver`](NameResolver::resolve_ident) with `default` being `None`. +/// +/// The `self_ident` parameter is used to specify the identifier for the returned value. +/// For most structs, `Self` is sufficient, but `Self::Variant` may be needed for enum variants. +/// +/// The `container_attrs` parameters, provided through `#[nu_value]` on the container, defines +/// global rules for the `FromValue` implementation. +/// This is used for the [`NameResolver`] to resolve the correct ident in the `Value`. +/// +/// This function is more complex than the equivalent for `IntoValue` due to additional error +/// handling: +/// - If a named field is missing in the `Value`, `ShellError::CantFindColumn` is returned. +/// - For unit structs, if the value is not `Value::Nothing`, `ShellError::CantConvert` is returned. +/// +/// The implementation avoids local variables for fields to prevent accidental shadowing, ensuring +/// that fields with similar names do not cause unexpected behavior. +/// This approach is not typically recommended in handwritten Rust, but it is acceptable for code +/// generation. +fn parse_value_via_fields( + fields: &Fields, + self_ident: impl ToTokens, + container_attrs: &ContainerAttributes, +) -> Result { + match fields { + Fields::Named(fields) => { + let mut name_resolver = NameResolver::new(); + let mut fields_ts: Vec = Vec::with_capacity(fields.named.len()); + for field in fields.named.iter() { + let member_attrs = MemberAttributes::parse_attrs(&field.attrs)?; + let ident = field.ident.as_ref().expect("named has idents"); + let ident_s = + name_resolver.resolve_ident(ident, container_attrs, &member_attrs, None)?; + let ty = &field.ty; + fields_ts.push(match (type_is_option(ty), member_attrs.default) { + (true, _) => quote! { + #ident: record + .remove(#ident_s) + .map(|v| <#ty as nu_protocol::FromValue>::from_value(v)) + .transpose()? + .flatten() + }, + (false, false) => quote! { + #ident: <#ty as nu_protocol::FromValue>::from_value( + record + .remove(#ident_s) + .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { + col_name: std::string::ToString::to_string(#ident_s), + span: std::option::Option::None, + src_span: span + })?, + )? + }, + (false, true) => quote! { + #ident: record + .remove(#ident_s) + .map(|v| <#ty as nu_protocol::FromValue>::from_value(v)) + .transpose()? + .unwrap_or_default() + }, + }); + } + Ok(quote! { + let span = v.span(); + let mut record = v.into_record()?; + std::result::Result::Ok(#self_ident {#(#fields_ts),*}) + }) + } + f @ Fields::Unnamed(fields) => { + attributes::deny_fields(f)?; + let fields = fields.unnamed.iter().enumerate().map(|(i, field)| { + let ty = &field.ty; + quote! {{ + <#ty as nu_protocol::FromValue>::from_value( + deque + .pop_front() + .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { + col_name: std::string::ToString::to_string(&#i), + span: std::option::Option::None, + src_span: span + })?, + )? + }} + }); + Ok(quote! { + let span = v.span(); + let list = v.into_list()?; + let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list); + std::result::Result::Ok(#self_ident(#(#fields),*)) + }) + } + Fields::Unit => Ok(quote! { + match v { + nu_protocol::Value::Nothing {..} => Ok(#self_ident), + v => std::result::Result::Err(nu_protocol::ShellError::CantConvert { + to_type: std::string::ToString::to_string(&::expected_type()), + from_type: std::string::ToString::to_string(&v.get_type()), + span: v.span(), + help: std::option::Option::None + }) + } + }), + } +} + +const FULLY_QUALIFIED_OPTION: &str = "std::option::Option"; +const PARTIALLY_QUALIFIED_OPTION: &str = "option::Option"; +const PRELUDE_OPTION: &str = "Option"; + +/// Check if the field type is an `Option`. +/// +/// This function checks if a given type is an `Option`. +/// We assume that an `Option` is [`std::option::Option`] because we can't see the whole code and +/// can't ask the compiler itself. +/// If the `Option` type isn't `std::option::Option`, the user will get a compile error due to a +/// type mismatch. +/// It's very unusual for people to override `Option`, so this should rarely be an issue. +/// +/// When [rust#63084](https://github.com/rust-lang/rust/issues/63084) is resolved, we can use +/// [`std::any::type_name`] for a static assertion check to get a more direct error messages. +fn type_is_option(ty: &Type) -> bool { + let s = ty.to_token_stream().to_string(); + s.starts_with(PRELUDE_OPTION) + || s.starts_with(PARTIALLY_QUALIFIED_OPTION) + || s.starts_with(FULLY_QUALIFIED_OPTION) +} diff --git a/nushell/crates/nu-derive-value/src/into.rs b/nushell/crates/nu-derive-value/src/into.rs new file mode 100644 index 0000000..de34a89 --- /dev/null +++ b/nushell/crates/nu-derive-value/src/into.rs @@ -0,0 +1,291 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, quote}; +use syn::{ + Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident, Index, + spanned::Spanned, +}; + +use crate::{ + attributes::{self, ContainerAttributes, MemberAttributes, ParseAttrs}, + case::Case, + names::NameResolver, +}; + +#[derive(Debug)] +pub struct IntoValue; +type DeriveError = super::error::DeriveError; +type Result = std::result::Result; + +/// Inner implementation of the `#[derive(IntoValue)]` macro for structs and enums. +/// +/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`. +/// +/// This function directs the `IntoValue` trait derivation to the correct implementation based on +/// the input type: +/// - For structs: [`struct_into_value`] +/// - For enums: [`enum_into_value`] +/// - Unions are not supported and will return an error. +pub fn derive_into_value(input: TokenStream2) -> Result { + let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?; + match input.data { + Data::Struct(data_struct) => Ok(struct_into_value( + input.ident, + data_struct, + input.generics, + input.attrs, + )?), + Data::Enum(data_enum) => Ok(enum_into_value( + input.ident, + data_enum, + input.generics, + input.attrs, + )?), + Data::Union(_) => Err(DeriveError::UnsupportedUnions), + } +} + +/// Implements the `#[derive(IntoValue)]` macro for structs. +/// +/// Automatically derives the `IntoValue` trait for any struct where each field implements +/// `IntoValue`. +/// For structs with named fields, the derived implementation creates a `Value::Record` using the +/// struct fields as keys. +/// The specific keys are resolved by [`NameResolver`](NameResolver::resolve_ident). +/// Each field value is converted using the `IntoValue::into_value` method. +/// For structs with unnamed fields, this generates a `Value::List` with each field in the list. +/// For unit structs, this generates `Value::Nothing`, because there is no data. +/// +/// This function provides the signature and prepares the call to the [`fields_return_value`] +/// function which does the heavy lifting of creating the `Value` calls. +/// +/// # Examples +/// +/// These examples show what the macro would generate. +/// +/// Struct with named fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Pet { +/// name: String, +/// age: u8, +/// favorite_toy: Option, +/// } +/// +/// impl nu_protocol::IntoValue for Pet { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// nu_protocol::Value::record(nu_protocol::record! { +/// "name" => nu_protocol::IntoValue::into_value(self.name, span), +/// "age" => nu_protocol::IntoValue::into_value(self.age, span), +/// "favorite_toy" => nu_protocol::IntoValue::into_value(self.favorite_toy, span), +/// }, span) +/// } +/// } +/// ``` +/// +/// Struct with unnamed fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Color(u8, u8, u8); +/// +/// impl nu_protocol::IntoValue for Color { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// nu_protocol::Value::list(vec![ +/// nu_protocol::IntoValue::into_value(self.0, span), +/// nu_protocol::IntoValue::into_value(self.1, span), +/// nu_protocol::IntoValue::into_value(self.2, span), +/// ], span) +/// } +/// } +/// ``` +/// +/// Unit struct: +/// ```rust +/// #[derive(IntoValue)] +/// struct Unicorn; +/// +/// impl nu_protocol::IntoValue for Unicorn { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// nu_protocol::Value::nothing(span) +/// } +/// } +/// ``` +fn struct_into_value( + ident: Ident, + data: DataStruct, + generics: Generics, + attrs: Vec, +) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let record = match &data.fields { + Fields::Named(fields) => { + let accessor = fields + .named + .iter() + .map(|field| field.ident.as_ref().expect("named has idents")) + .map(|ident| quote!(self.#ident)); + fields_return_value(&data.fields, accessor, &container_attrs)? + } + Fields::Unnamed(fields) => { + let accessor = fields + .unnamed + .iter() + .enumerate() + .map(|(n, _)| Index::from(n)) + .map(|index| quote!(self.#index)); + fields_return_value(&data.fields, accessor, &container_attrs)? + } + Fields::Unit => quote!(nu_protocol::Value::nothing(span)), + }; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + Ok(quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause { + fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { + #record + } + } + }) +} + +/// Implements the `#[derive(IntoValue)]` macro for enums. +/// +/// This function implements the derive macro `IntoValue` for enums. +/// Currently, only unit enum variants are supported as it is not clear how other types of enums +/// should be represented in a `Value`. +/// For simple enums, we represent the enum as a `Value::String`. +/// For other types of variants, we return an error. +/// +/// The variant name used in the `Value::String` is resolved by the +/// [`NameResolver`](NameResolver::resolve_ident) with the `default` being [`Case::Snake`]. +/// The implementation matches over all variants, uses the appropriate variant name, and constructs +/// a `Value::String`. +/// +/// This is how such a derived implementation looks: +/// ```rust +/// #[derive(IntoValue)] +/// enum Weather { +/// Sunny, +/// Cloudy, +/// #[nu_value(rename = "rain")] +/// Raining +/// } +/// +/// impl nu_protocol::IntoValue for Weather { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// match self { +/// Self::Sunny => nu_protocol::Value::string("sunny", span), +/// Self::Cloudy => nu_protocol::Value::string("cloudy", span), +/// Self::Raining => nu_protocol::Value::string("rain", span), +/// } +/// } +/// } +/// ``` +fn enum_into_value( + ident: Ident, + data: DataEnum, + generics: Generics, + attrs: Vec, +) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let mut name_resolver = NameResolver::new(); + let arms: Vec = data + .variants + .into_iter() + .map(|variant| { + let member_attrs = MemberAttributes::parse_attrs(variant.attrs.iter())?; + let ident = variant.ident; + let ident_s = name_resolver.resolve_ident( + &ident, + &container_attrs, + &member_attrs, + Case::Snake, + )?; + match &variant.fields { + // In the future we can implement more complex enums here. + Fields::Named(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unit => { + Ok(quote!(Self::#ident => nu_protocol::Value::string(#ident_s, span))) + } + } + }) + .collect::>()?; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + Ok(quote! { + impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause { + fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { + match self { + #(#arms,)* + } + } + } + }) +} + +/// Constructs the final `Value` that the macro generates. +/// +/// This function handles the construction of the final `Value` that the macro generates, primarily +/// for structs. +/// It takes three parameters: `fields`, which allows iterating over each field of a data type, +/// `accessor`, which generalizes data access, and `container_attrs`, which is used for the +/// [`NameResolver`]. +/// +/// - **Field Keys**: +/// The field key is field name of the input struct and resolved the +/// [`NameResolver`](NameResolver::resolve_ident). +/// +/// - **Fields Type**: +/// - Determines whether to generate a `Value::Record`, `Value::List`, or `Value::Nothing` based +/// on the nature of the fields. +/// - Named fields are directly used to generate the record key, as described above. +/// +/// - **Accessor**: +/// - Generalizes how data is accessed for different data types. +/// - For named fields in structs, this is typically `self.field_name`. +/// - For unnamed fields (e.g., tuple structs), it should be an iterator similar to named fields +/// but accessing fields like `self.0`. +/// - For unit structs, this parameter is ignored. +/// +/// This design allows the same function to potentially handle both structs and enums with data +/// variants in the future. +fn fields_return_value( + fields: &Fields, + accessor: impl Iterator, + container_attrs: &ContainerAttributes, +) -> Result { + match fields { + Fields::Named(fields) => { + let mut name_resolver = NameResolver::new(); + let mut items: Vec = Vec::with_capacity(fields.named.len()); + for (field, accessor) in fields.named.iter().zip(accessor) { + let member_attrs = MemberAttributes::parse_attrs(field.attrs.iter())?; + let ident = field.ident.as_ref().expect("named has idents"); + let field = + name_resolver.resolve_ident(ident, container_attrs, &member_attrs, None)?; + items.push(quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span))); + } + Ok(quote! { + nu_protocol::Value::record(nu_protocol::record! { + #(#items),* + }, span) + }) + } + f @ Fields::Unnamed(fields) => { + attributes::deny_fields(f)?; + let items = + fields.unnamed.iter().zip(accessor).map( + |(_, accessor)| quote!(nu_protocol::IntoValue::into_value(#accessor, span)), + ); + Ok(quote!(nu_protocol::Value::list( + std::vec![#(#items),*], + span + ))) + } + Fields::Unit => Ok(quote!(nu_protocol::Value::nothing(span))), + } +} diff --git a/nushell/crates/nu-derive-value/src/lib.rs b/nushell/crates/nu-derive-value/src/lib.rs new file mode 100644 index 0000000..1324516 --- /dev/null +++ b/nushell/crates/nu-derive-value/src/lib.rs @@ -0,0 +1,71 @@ +//! Macro implementations of `#[derive(FromValue, IntoValue)]`. +//! +//! As this crate is a [`proc_macro`] crate, it is only allowed to export +//! [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html). +//! Therefore, it only exports [`IntoValue`] and [`FromValue`]. +//! +//! To get documentation for other functions and types used in this crate, run +//! `cargo doc -p nu-derive-value --document-private-items`. +//! +//! This crate uses a lot of +//! [`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/1.0.24/proc_macro2/struct.TokenStream.html) +//! as `TokenStream2` to allow testing the behavior of the macros directly, including the output +//! token stream or if the macro errors as expected. +//! The tests for functionality can be found in `nu_protocol::value::test_derive`. +//! +//! This documentation is often less reference-heavy than typical Rust documentation. +//! This is because this crate is a dependency for `nu_protocol`, and linking to it would create a +//! cyclic dependency. +//! Also all examples in the documentation aren't tested as this crate cannot be compiled as a +//! normal library very easily. +//! This might change in the future if cargo allows building a proc-macro crate differently for +//! `cfg(doctest)` as they are already doing for `cfg(test)`. +//! +//! The generated code from the derive macros tries to be as +//! [hygienic](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene) as possible. +//! This ensures that the macro can be called anywhere without requiring specific imports. +//! This results in obtuse code, which isn't recommended for manual, handwritten Rust +//! but ensures that no other code may influence this generated code or vice versa. + +use proc_macro::TokenStream; +use proc_macro_error2::{Diagnostic, proc_macro_error}; +use proc_macro2::TokenStream as TokenStream2; + +mod attributes; +mod case; +mod error; +mod from; +mod into; +mod names; +#[cfg(test)] +mod tests; + +const HELPER_ATTRIBUTE: &str = "nu_value"; + +/// Derive macro generating an impl of the trait `IntoValue`. +/// +/// For further information, see the docs on the trait itself. +#[proc_macro_derive(IntoValue, attributes(nu_value))] +#[proc_macro_error] +pub fn derive_into_value(input: TokenStream) -> TokenStream { + let input = TokenStream2::from(input); + let output = match into::derive_into_value(input) { + Ok(output) => output, + Err(e) => Diagnostic::from(e).abort(), + }; + TokenStream::from(output) +} + +/// Derive macro generating an impl of the trait `FromValue`. +/// +/// For further information, see the docs on the trait itself. +#[proc_macro_derive(FromValue, attributes(nu_value))] +#[proc_macro_error] +pub fn derive_from_value(input: TokenStream) -> TokenStream { + let input = TokenStream2::from(input); + let output = match from::derive_from_value(input) { + Ok(output) => output, + Err(e) => Diagnostic::from(e).abort(), + }; + TokenStream::from(output) +} diff --git a/nushell/crates/nu-derive-value/src/names.rs b/nushell/crates/nu-derive-value/src/names.rs new file mode 100644 index 0000000..825f3e4 --- /dev/null +++ b/nushell/crates/nu-derive-value/src/names.rs @@ -0,0 +1,61 @@ +use proc_macro2::Span; +use std::collections::HashMap; +use syn::Ident; +use syn::ext::IdentExt; + +use crate::attributes::{ContainerAttributes, MemberAttributes}; +use crate::case::{Case, Casing}; +use crate::error::DeriveError; + +#[derive(Debug, Default)] +pub struct NameResolver { + seen_names: HashMap, +} + +impl NameResolver { + pub fn new() -> Self { + Self::default() + } + + /// Resolves an identifier using attributes and ensures its uniqueness. + /// + /// The identifier is transformed according to these rules: + /// - If [`MemberAttributes::rename`] is set, this explicitly renamed value is used. + /// The value is defined by the helper attribute `#[nu_value(rename = "...")]` on a member. + /// - If the above is not set but [`ContainerAttributes::rename_all`] is, the identifier + /// undergoes case conversion as specified by the helper attribute + /// `#[nu_value(rename_all = "...")]` on the container (struct or enum). + /// - If neither renaming attribute is set, the function applies the case conversion provided + /// by the `default` parameter. + /// If `default` is `None`, the identifier remains unchanged. + /// + /// This function checks the transformed identifier against previously seen identifiers to + /// ensure it is unique. + /// If a duplicate identifier is detected, it returns [`DeriveError::NonUniqueName`]. + pub fn resolve_ident( + &mut self, + ident: &Ident, + container_attrs: &ContainerAttributes, + member_attrs: &MemberAttributes, + default: impl Into>, + ) -> Result> { + let span = ident.span(); + let ident = if let Some(rename) = &member_attrs.rename { + rename.clone() + } else { + let case = container_attrs.rename_all.or(default.into()); + ident.unraw().to_case(case) + }; + + if let Some(seen) = self.seen_names.get(&ident) { + return Err(DeriveError::NonUniqueName { + name: ident.to_string(), + first: *seen, + second: span, + }); + } + + self.seen_names.insert(ident.clone(), span); + Ok(ident) + } +} diff --git a/nushell/crates/nu-derive-value/src/tests.rs b/nushell/crates/nu-derive-value/src/tests.rs new file mode 100644 index 0000000..87a4b7c --- /dev/null +++ b/nushell/crates/nu-derive-value/src/tests.rs @@ -0,0 +1,242 @@ +// These tests only check that the derive macros throw the relevant errors. +// Functionality of the derived types is tested in nu_protocol::value::test_derive. + +use crate::error::DeriveError; +use crate::from::derive_from_value; +use crate::into::derive_into_value; +use quote::quote; + +#[test] +fn unsupported_unions() { + let input = quote! { + #[nu_value] + union SomeUnion { + f1: u32, + f2: f32, + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnsupportedUnions)), + "expected `DeriveError::UnsupportedUnions`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnsupportedUnions)), + "expected `DeriveError::UnsupportedUnions`, got {:?}", + into_res + ); +} + +#[test] +fn unsupported_enums() { + let input = quote! { + #[nu_value(rename_all = "SCREAMING_SNAKE_CASE")] + enum ComplexEnum { + Unit, + Unnamed(u32, f32), + Named { + u: u32, + f: f32, + } + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnsupportedEnums { .. })), + "expected `DeriveError::UnsupportedEnums`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnsupportedEnums { .. })), + "expected `DeriveError::UnsupportedEnums`, got {:?}", + into_res + ); +} + +#[test] +fn unexpected_attribute() { + let input = quote! { + #[nu_value(what)] + enum SimpleEnum { + A, + B, + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + into_res + ); +} + +#[test] +fn unexpected_attribute_on_struct_field() { + let input = quote! { + struct SimpleStruct { + #[nu_value(what)] + field_a: i32, + field_b: String, + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + into_res + ); +} + +#[test] +fn unexpected_attribute_on_enum_variant() { + let input = quote! { + enum SimpleEnum { + #[nu_value(what)] + A, + B, + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + into_res + ); +} + +#[test] +fn invalid_attribute_position_in_tuple_struct() { + let input = quote! { + struct SimpleTupleStruct( + #[nu_value(what)] + i32, + String, + ); + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!( + from_res, + Err(DeriveError::InvalidAttributePosition { attribute_span: _ }) + ), + "expected `DeriveError::InvalidAttributePosition`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!( + into_res, + Err(DeriveError::InvalidAttributePosition { attribute_span: _ }) + ), + "expected `DeriveError::InvalidAttributePosition`, got {:?}", + into_res + ); +} + +#[test] +fn invalid_attribute_value() { + let input = quote! { + #[nu_value(rename_all = "CrazY-CasE")] + enum SimpleEnum { + A, + B + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::InvalidAttributeValue { .. })), + "expected `DeriveError::InvalidAttributeValue`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::InvalidAttributeValue { .. })), + "expected `DeriveError::InvalidAttributeValue`, got {:?}", + into_res + ); +} + +#[test] +fn non_unique_struct_keys() { + let input = quote! { + struct DuplicateStruct { + #[nu_value(rename = "field")] + some_field: (), + field: (), + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::NonUniqueName { .. })), + "expected `DeriveError::NonUniqueName`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::NonUniqueName { .. })), + "expected `DeriveError::NonUniqueName`, got {:?}", + into_res + ); +} + +#[test] +fn non_unique_enum_variants() { + let input = quote! { + enum DuplicateEnum { + #[nu_value(rename = "variant")] + SomeVariant, + Variant + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::NonUniqueName { .. })), + "expected `DeriveError::NonUniqueName`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::NonUniqueName { .. })), + "expected `DeriveError::NonUniqueName`, got {:?}", + into_res + ); +} diff --git a/nushell/crates/nu-engine/Cargo.toml b/nushell/crates/nu-engine/Cargo.toml new file mode 100644 index 0000000..8f6aba6 --- /dev/null +++ b/nushell/crates/nu-engine/Cargo.toml @@ -0,0 +1,32 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Nushell's evaluation engine" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-engine" +edition = "2024" +license = "MIT" +name = "nu-engine" +version = "0.105.2" + +[lib] +bench = false + +[lints] +workspace = true + +[dependencies] +nu-protocol = { path = "../nu-protocol", version = "0.105.2", default-features = false } +nu-path = { path = "../nu-path", version = "0.105.2" } +nu-glob = { path = "../nu-glob", version = "0.105.2" } +nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false } +log = { workspace = true } + +[features] +default = ["os"] +os = [ + "nu-protocol/os", + "nu-utils/os", +] +plugin = [ + "nu-protocol/plugin", + "os", +] diff --git a/nushell/crates/nu-engine/LICENSE b/nushell/crates/nu-engine/LICENSE new file mode 100644 index 0000000..ae174e8 --- /dev/null +++ b/nushell/crates/nu-engine/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nushell/crates/nu-engine/README.md b/nushell/crates/nu-engine/README.md new file mode 100644 index 0000000..d5198cf --- /dev/null +++ b/nushell/crates/nu-engine/README.md @@ -0,0 +1,9 @@ +This crate primarily drives the evaluation of expressions. + +(Some overlap with nu-protocol) + +- Provides `CallExt` + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/nushell/crates/nu-engine/src/call_ext.rs b/nushell/crates/nu-engine/src/call_ext.rs new file mode 100644 index 0000000..7698a50 --- /dev/null +++ b/nushell/crates/nu-engine/src/call_ext.rs @@ -0,0 +1,400 @@ +use crate::eval_expression; +use nu_protocol::{ + FromValue, ShellError, Span, Value, ast, + debugger::WithoutDebug, + engine::{self, EngineState, Stack, StateWorkingSet}, + eval_const::eval_constant, + ir, +}; + +pub trait CallExt { + /// Check if a boolean flag is set (i.e. `--bool` or `--bool=true`) + fn has_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + flag_name: &str, + ) -> Result; + + fn get_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError>; + + /// Efficiently get the span of a flag argument + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option; + + fn rest( + &self, + engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError>; + + fn opt( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError>; + + fn opt_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result, ShellError>; + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result; + + fn req_parser_info( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result; + + /// True if the command has any positional or rest arguments, excluding before the given index. + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool; +} + +impl CallExt for ast::Call { + fn has_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + flag_name: &str, + ) -> Result { + for name in self.named_iter() { + if flag_name == name.0.item { + return if let Some(expr) = &name.2 { + // Check --flag=false + let stack = &mut stack.use_call_arg_out_dest(); + let result = eval_expression::(engine_state, stack, expr)?; + match result { + Value::Bool { val, .. } => Ok(val), + _ => Err(ShellError::CantConvert { + to_type: "bool".into(), + from_type: result.get_type().to_string(), + span: result.span(), + help: Some("".into()), + }), + } + } else { + Ok(true) + }; + } + } + + Ok(false) + } + + fn get_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError> { + if let Some(expr) = self.get_flag_expr(name) { + let stack = &mut stack.use_call_arg_out_dest(); + let result = eval_expression::(engine_state, stack, expr)?; + FromValue::from_value(result).map(Some) + } else { + Ok(None) + } + } + + fn get_flag_span(&self, _stack: &Stack, name: &str) -> Option { + self.get_named_arg(name).map(|arg| arg.span) + } + + fn rest( + &self, + engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError> { + let stack = &mut stack.use_call_arg_out_dest(); + self.rest_iter_flattened(starting_pos, |expr| { + eval_expression::(engine_state, stack, expr) + })? + .into_iter() + .map(FromValue::from_value) + .collect() + } + + fn opt( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError> { + if let Some(expr) = self.positional_nth(pos) { + let stack = &mut stack.use_call_arg_out_dest(); + let result = eval_expression::(engine_state, stack, expr)?; + FromValue::from_value(result).map(Some) + } else { + Ok(None) + } + } + + fn opt_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result, ShellError> { + if let Some(expr) = self.positional_nth(pos) { + let result = eval_constant(working_set, expr)?; + FromValue::from_value(result).map(Some) + } else { + Ok(None) + } + } + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result { + if let Some(expr) = self.positional_nth(pos) { + let stack = &mut stack.use_call_arg_out_dest(); + let result = eval_expression::(engine_state, stack, expr)?; + FromValue::from_value(result) + } else if self.positional_len() == 0 { + Err(ShellError::AccessEmptyContent { span: self.head }) + } else { + Err(ShellError::AccessBeyondEnd { + max_idx: self.positional_len() - 1, + span: self.head, + }) + } + } + + fn req_parser_info( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result { + if let Some(expr) = self.get_parser_info(name) { + let stack = &mut stack.use_call_arg_out_dest(); + let result = eval_expression::(engine_state, stack, expr)?; + FromValue::from_value(result) + } else if self.parser_info.is_empty() { + Err(ShellError::AccessEmptyContent { span: self.head }) + } else { + Err(ShellError::AccessBeyondEnd { + max_idx: self.parser_info.len() - 1, + span: self.head, + }) + } + } + + fn has_positional_args(&self, _stack: &Stack, starting_pos: usize) -> bool { + self.rest_iter(starting_pos).next().is_some() + } +} + +impl CallExt for ir::Call { + fn has_flag( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + flag_name: &str, + ) -> Result { + Ok(self + .named_iter(stack) + .find(|(name, _)| name.item == flag_name) + .is_some_and(|(_, value)| { + // Handle --flag=false + !matches!(value, Some(Value::Bool { val: false, .. })) + })) + } + + fn get_flag( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError> { + if let Some(val) = self.get_named_arg(stack, name) { + T::from_value(val.clone()).map(Some) + } else { + Ok(None) + } + } + + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option { + self.named_iter(stack) + .find_map(|(i_name, _)| (i_name.item == name).then_some(i_name.span)) + } + + fn rest( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError> { + self.rest_iter_flattened(stack, starting_pos)? + .into_iter() + .map(T::from_value) + .collect() + } + + fn opt( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError> { + self.positional_iter(stack) + .nth(pos) + .cloned() + .map(T::from_value) + .transpose() + } + + fn opt_const( + &self, + _working_set: &StateWorkingSet, + _pos: usize, + ) -> Result, ShellError> { + Err(ShellError::IrEvalError { + msg: "const evaluation is not yet implemented on ir::Call".into(), + span: Some(self.head), + }) + } + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result { + if let Some(val) = self.opt(engine_state, stack, pos)? { + Ok(val) + } else if self.positional_len(stack) == 0 { + Err(ShellError::AccessEmptyContent { span: self.head }) + } else { + Err(ShellError::AccessBeyondEnd { + max_idx: self.positional_len(stack) - 1, + span: self.head, + }) + } + } + + fn req_parser_info( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result { + // FIXME: this depends on the AST evaluator. We can fix this by making the parser info an + // enum rather than using expressions. It's not clear that evaluation of this is ever really + // needed. + if let Some(expr) = self.get_parser_info(stack, name) { + let expr = expr.clone(); + let stack = &mut stack.use_call_arg_out_dest(); + let result = eval_expression::(engine_state, stack, &expr)?; + FromValue::from_value(result) + } else { + Err(ShellError::CantFindColumn { + col_name: name.into(), + span: None, + src_span: self.head, + }) + } + } + + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool { + self.rest_iter(stack, starting_pos).next().is_some() + } +} + +macro_rules! proxy { + ($self:ident . $method:ident ($($param:expr),*)) => (match &$self.inner { + engine::CallImpl::AstRef(call) => call.$method($($param),*), + engine::CallImpl::AstBox(call) => call.$method($($param),*), + engine::CallImpl::IrRef(call) => call.$method($($param),*), + engine::CallImpl::IrBox(call) => call.$method($($param),*), + }) +} + +impl CallExt for engine::Call<'_> { + fn has_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + flag_name: &str, + ) -> Result { + proxy!(self.has_flag(engine_state, stack, flag_name)) + } + + fn get_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError> { + proxy!(self.get_flag(engine_state, stack, name)) + } + + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option { + proxy!(self.get_flag_span(stack, name)) + } + + fn rest( + &self, + engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError> { + proxy!(self.rest(engine_state, stack, starting_pos)) + } + + fn opt( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError> { + proxy!(self.opt(engine_state, stack, pos)) + } + + fn opt_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result, ShellError> { + proxy!(self.opt_const(working_set, pos)) + } + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result { + proxy!(self.req(engine_state, stack, pos)) + } + + fn req_parser_info( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result { + proxy!(self.req_parser_info(engine_state, stack, name)) + } + + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool { + proxy!(self.has_positional_args(stack, starting_pos)) + } +} diff --git a/nushell/crates/nu-engine/src/closure_eval.rs b/nushell/crates/nu-engine/src/closure_eval.rs new file mode 100644 index 0000000..6778c09 --- /dev/null +++ b/nushell/crates/nu-engine/src/closure_eval.rs @@ -0,0 +1,275 @@ +use crate::{ + EvalBlockWithEarlyReturnFn, eval_block_with_early_return, get_eval_block_with_early_return, +}; +use nu_protocol::{ + IntoPipelineData, PipelineData, ShellError, Value, + ast::Block, + debugger::{WithDebug, WithoutDebug}, + engine::{Closure, EngineState, EnvVars, Stack}, +}; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + sync::Arc, +}; + +fn eval_fn(debug: bool) -> EvalBlockWithEarlyReturnFn { + if debug { + eval_block_with_early_return:: + } else { + eval_block_with_early_return:: + } +} + +/// [`ClosureEval`] is used to repeatedly evaluate a closure with different values/inputs. +/// +/// [`ClosureEval`] has a builder API. +/// It is first created via [`ClosureEval::new`], +/// then has arguments added via [`ClosureEval::add_arg`], +/// and then can be run using [`ClosureEval::run_with_input`]. +/// +/// ```no_run +/// # use nu_protocol::{PipelineData, Value}; +/// # use nu_engine::ClosureEval; +/// # let engine_state = unimplemented!(); +/// # let stack = unimplemented!(); +/// # let closure = unimplemented!(); +/// let mut closure = ClosureEval::new(engine_state, stack, closure); +/// let iter = Vec::::new() +/// .into_iter() +/// .map(move |value| closure.add_arg(value).run_with_input(PipelineData::Empty)); +/// ``` +/// +/// Many closures follow a simple, common scheme where the pipeline input and the first argument are the same value. +/// In this case, use [`ClosureEval::run_with_value`]: +/// +/// ```no_run +/// # use nu_protocol::{PipelineData, Value}; +/// # use nu_engine::ClosureEval; +/// # let engine_state = unimplemented!(); +/// # let stack = unimplemented!(); +/// # let closure = unimplemented!(); +/// let mut closure = ClosureEval::new(engine_state, stack, closure); +/// let iter = Vec::::new() +/// .into_iter() +/// .map(move |value| closure.run_with_value(value)); +/// ``` +/// +/// Environment isolation and other cleanup is handled by [`ClosureEval`], +/// so nothing needs to be done following [`ClosureEval::run_with_input`] or [`ClosureEval::run_with_value`]. +pub struct ClosureEval { + engine_state: EngineState, + stack: Stack, + block: Arc, + arg_index: usize, + env_vars: Vec>, + env_hidden: Arc>>, + eval: EvalBlockWithEarlyReturnFn, +} + +impl ClosureEval { + /// Create a new [`ClosureEval`]. + pub fn new(engine_state: &EngineState, stack: &Stack, closure: Closure) -> Self { + let engine_state = engine_state.clone(); + let stack = stack.captures_to_stack(closure.captures); + let block = engine_state.get_block(closure.block_id).clone(); + let env_vars = stack.env_vars.clone(); + let env_hidden = stack.env_hidden.clone(); + let eval = get_eval_block_with_early_return(&engine_state); + + Self { + engine_state, + stack, + block, + arg_index: 0, + env_vars, + env_hidden, + eval, + } + } + + pub fn new_preserve_out_dest( + engine_state: &EngineState, + stack: &Stack, + closure: Closure, + ) -> Self { + let engine_state = engine_state.clone(); + let stack = stack.captures_to_stack_preserve_out_dest(closure.captures); + let block = engine_state.get_block(closure.block_id).clone(); + let env_vars = stack.env_vars.clone(); + let env_hidden = stack.env_hidden.clone(); + let eval = get_eval_block_with_early_return(&engine_state); + + Self { + engine_state, + stack, + block, + arg_index: 0, + env_vars, + env_hidden, + eval, + } + } + + /// Sets whether to enable debugging when evaluating the closure. + /// + /// By default, this is controlled by the [`EngineState`] used to create this [`ClosureEval`]. + pub fn debug(&mut self, debug: bool) -> &mut Self { + self.eval = eval_fn(debug); + self + } + + fn try_add_arg(&mut self, value: Cow) { + if let Some(var_id) = self + .block + .signature + .get_positional(self.arg_index) + .and_then(|var| var.var_id) + { + self.stack.add_var(var_id, value.into_owned()); + self.arg_index += 1; + } + } + + /// Add an argument [`Value`] to the closure. + /// + /// Multiple [`add_arg`](Self::add_arg) calls can be chained together, + /// but make sure that arguments are added based on their positional order. + pub fn add_arg(&mut self, value: Value) -> &mut Self { + self.try_add_arg(Cow::Owned(value)); + self + } + + /// Run the closure, passing the given [`PipelineData`] as input. + /// + /// Any arguments should be added beforehand via [`add_arg`](Self::add_arg). + pub fn run_with_input(&mut self, input: PipelineData) -> Result { + self.arg_index = 0; + self.stack.with_env(&self.env_vars, &self.env_hidden); + (self.eval)(&self.engine_state, &mut self.stack, &self.block, input) + } + + /// Run the closure using the given [`Value`] as both the pipeline input and the first argument. + /// + /// Using this function after or in combination with [`add_arg`](Self::add_arg) is most likely an error. + /// This function is equivalent to `self.add_arg(value)` followed by `self.run_with_input(value.into_pipeline_data())`. + pub fn run_with_value(&mut self, value: Value) -> Result { + self.try_add_arg(Cow::Borrowed(&value)); + self.run_with_input(value.into_pipeline_data()) + } +} + +/// [`ClosureEvalOnce`] is used to evaluate a closure a single time. +/// +/// [`ClosureEvalOnce`] has a builder API. +/// It is first created via [`ClosureEvalOnce::new`], +/// then has arguments added via [`ClosureEvalOnce::add_arg`], +/// and then can be run using [`ClosureEvalOnce::run_with_input`]. +/// +/// ```no_run +/// # use nu_protocol::{ListStream, PipelineData, PipelineIterator}; +/// # use nu_engine::ClosureEvalOnce; +/// # let engine_state = unimplemented!(); +/// # let stack = unimplemented!(); +/// # let closure = unimplemented!(); +/// # let value = unimplemented!(); +/// let result = ClosureEvalOnce::new(engine_state, stack, closure) +/// .add_arg(value) +/// .run_with_input(PipelineData::Empty); +/// ``` +/// +/// Many closures follow a simple, common scheme where the pipeline input and the first argument are the same value. +/// In this case, use [`ClosureEvalOnce::run_with_value`]: +/// +/// ```no_run +/// # use nu_protocol::{PipelineData, PipelineIterator}; +/// # use nu_engine::ClosureEvalOnce; +/// # let engine_state = unimplemented!(); +/// # let stack = unimplemented!(); +/// # let closure = unimplemented!(); +/// # let value = unimplemented!(); +/// let result = ClosureEvalOnce::new(engine_state, stack, closure).run_with_value(value); +/// ``` +pub struct ClosureEvalOnce<'a> { + engine_state: &'a EngineState, + stack: Stack, + block: &'a Block, + arg_index: usize, + eval: EvalBlockWithEarlyReturnFn, +} + +impl<'a> ClosureEvalOnce<'a> { + /// Create a new [`ClosureEvalOnce`]. + pub fn new(engine_state: &'a EngineState, stack: &Stack, closure: Closure) -> Self { + let block = engine_state.get_block(closure.block_id); + let eval = get_eval_block_with_early_return(engine_state); + Self { + engine_state, + stack: stack.captures_to_stack(closure.captures), + block, + arg_index: 0, + eval, + } + } + + pub fn new_preserve_out_dest( + engine_state: &'a EngineState, + stack: &Stack, + closure: Closure, + ) -> Self { + let block = engine_state.get_block(closure.block_id); + let eval = get_eval_block_with_early_return(engine_state); + Self { + engine_state, + stack: stack.captures_to_stack_preserve_out_dest(closure.captures), + block, + arg_index: 0, + eval, + } + } + + /// Sets whether to enable debugging when evaluating the closure. + /// + /// By default, this is controlled by the [`EngineState`] used to create this [`ClosureEvalOnce`]. + pub fn debug(mut self, debug: bool) -> Self { + self.eval = eval_fn(debug); + self + } + + fn try_add_arg(&mut self, value: Cow) { + if let Some(var_id) = self + .block + .signature + .get_positional(self.arg_index) + .and_then(|var| var.var_id) + { + self.stack.add_var(var_id, value.into_owned()); + self.arg_index += 1; + } + } + + /// Add an argument [`Value`] to the closure. + /// + /// Multiple [`add_arg`](Self::add_arg) calls can be chained together, + /// but make sure that arguments are added based on their positional order. + pub fn add_arg(mut self, value: Value) -> Self { + self.try_add_arg(Cow::Owned(value)); + self + } + + /// Run the closure, passing the given [`PipelineData`] as input. + /// + /// Any arguments should be added beforehand via [`add_arg`](Self::add_arg). + pub fn run_with_input(mut self, input: PipelineData) -> Result { + (self.eval)(self.engine_state, &mut self.stack, self.block, input) + } + + /// Run the closure using the given [`Value`] as both the pipeline input and the first argument. + /// + /// Using this function after or in combination with [`add_arg`](Self::add_arg) is most likely an error. + /// This function is equivalent to `self.add_arg(value)` followed by `self.run_with_input(value.into_pipeline_data())`. + pub fn run_with_value(mut self, value: Value) -> Result { + self.try_add_arg(Cow::Borrowed(&value)); + self.run_with_input(value.into_pipeline_data()) + } +} diff --git a/nushell/crates/nu-engine/src/column.rs b/nushell/crates/nu-engine/src/column.rs new file mode 100644 index 0000000..483ca79 --- /dev/null +++ b/nushell/crates/nu-engine/src/column.rs @@ -0,0 +1,36 @@ +use nu_protocol::Value; +use std::collections::HashSet; + +pub fn get_columns(input: &[Value]) -> Vec { + let mut column_set = HashSet::new(); + let mut columns = Vec::new(); + for item in input { + let Value::Record { val, .. } = item else { + return vec![]; + }; + + for col in val.columns() { + if column_set.insert(col) { + columns.push(col.to_string()); + } + } + } + + columns +} + +// If a column doesn't exist in the input, return it. +pub fn nonexistent_column<'a, I>(inputs: &[String], columns: I) -> Option +where + I: IntoIterator, +{ + let set: HashSet<&String> = HashSet::from_iter(columns); + + for input in inputs { + if set.contains(input) { + continue; + } + return Some(input.clone()); + } + None +} diff --git a/nushell/crates/nu-engine/src/command_prelude.rs b/nushell/crates/nu-engine/src/command_prelude.rs new file mode 100644 index 0000000..702a6a4 --- /dev/null +++ b/nushell/crates/nu-engine/src/command_prelude.rs @@ -0,0 +1,10 @@ +pub use crate::CallExt; +pub use nu_protocol::{ + ByteStream, ByteStreamType, Category, ErrSpan, Example, IntoInterruptiblePipelineData, + IntoPipelineData, IntoSpanned, IntoValue, PipelineData, Record, ShellError, Signature, Span, + Spanned, SyntaxShape, Type, Value, + ast::CellPath, + engine::{Call, Command, EngineState, Stack, StateWorkingSet}, + record, + shell_error::{io::*, job::*}, +}; diff --git a/nushell/crates/nu-engine/src/compile/builder.rs b/nushell/crates/nu-engine/src/compile/builder.rs new file mode 100644 index 0000000..4d9117f --- /dev/null +++ b/nushell/crates/nu-engine/src/compile/builder.rs @@ -0,0 +1,599 @@ +use nu_protocol::{ + CompileError, IntoSpanned, RegId, Span, Spanned, + ast::Pattern, + ir::{DataSlice, Instruction, IrAstRef, IrBlock, Literal}, +}; + +/// A label identifier. Only exists while building code. Replaced with the actual target. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct LabelId(pub usize); + +/// Builds [`IrBlock`]s progressively by consuming instructions and handles register allocation. +#[derive(Debug)] +pub(crate) struct BlockBuilder { + pub(crate) block_span: Option, + pub(crate) instructions: Vec, + pub(crate) spans: Vec, + /// The actual instruction index that a label refers to. While building IR, branch targets are + /// specified as indices into this array rather than the true instruction index. This makes it + /// easier to make modifications to code, as just this array needs to be changed, and it's also + /// less error prone as during `finish()` we check to make sure all of the used labels have had + /// an index actually set. + pub(crate) labels: Vec>, + pub(crate) data: Vec, + pub(crate) ast: Vec>, + pub(crate) comments: Vec, + pub(crate) register_allocation_state: Vec, + pub(crate) file_count: u32, + pub(crate) loop_stack: Vec, +} + +impl BlockBuilder { + /// Starts a new block, with the first register (`%0`) allocated as input. + pub(crate) fn new(block_span: Option) -> Self { + BlockBuilder { + block_span, + instructions: vec![], + spans: vec![], + labels: vec![], + data: vec![], + ast: vec![], + comments: vec![], + register_allocation_state: vec![true], + file_count: 0, + loop_stack: vec![], + } + } + + /// Get the next unused register for code generation. + pub(crate) fn next_register(&mut self) -> Result { + if let Some(index) = self + .register_allocation_state + .iter_mut() + .position(|is_allocated| { + if !*is_allocated { + *is_allocated = true; + true + } else { + false + } + }) + { + Ok(RegId::new(index as u32)) + } else if self.register_allocation_state.len() < (u32::MAX as usize - 2) { + let reg_id = RegId::new(self.register_allocation_state.len() as u32); + self.register_allocation_state.push(true); + Ok(reg_id) + } else { + Err(CompileError::RegisterOverflow { + block_span: self.block_span, + }) + } + } + + /// Check if a register is initialized with a value. + pub(crate) fn is_allocated(&self, reg_id: RegId) -> bool { + self.register_allocation_state + .get(reg_id.get() as usize) + .is_some_and(|state| *state) + } + + /// Mark a register as initialized. + pub(crate) fn mark_register(&mut self, reg_id: RegId) -> Result<(), CompileError> { + if let Some(is_allocated) = self + .register_allocation_state + .get_mut(reg_id.get() as usize) + { + *is_allocated = true; + Ok(()) + } else { + Err(CompileError::RegisterOverflow { + block_span: self.block_span, + }) + } + } + + /// Mark a register as empty, so that it can be used again by something else. + #[track_caller] + pub(crate) fn free_register(&mut self, reg_id: RegId) -> Result<(), CompileError> { + let index = reg_id.get() as usize; + + if self + .register_allocation_state + .get(index) + .is_some_and(|is_allocated| *is_allocated) + { + self.register_allocation_state[index] = false; + Ok(()) + } else { + log::warn!("register {reg_id} uninitialized, builder = {self:#?}"); + Err(CompileError::RegisterUninitialized { + reg_id, + caller: std::panic::Location::caller().to_string(), + }) + } + } + + /// Define a label, which can be used by branch instructions. The target can optionally be + /// specified now. + pub(crate) fn label(&mut self, target_index: Option) -> LabelId { + let label_id = self.labels.len(); + self.labels.push(target_index); + LabelId(label_id) + } + + /// Change the target of a label. + pub(crate) fn set_label( + &mut self, + label_id: LabelId, + target_index: usize, + ) -> Result<(), CompileError> { + *self + .labels + .get_mut(label_id.0) + .ok_or(CompileError::UndefinedLabel { + label_id: label_id.0, + span: None, + })? = Some(target_index); + Ok(()) + } + + /// Insert an instruction into the block, automatically marking any registers populated by + /// the instruction, and freeing any registers consumed by the instruction. + #[track_caller] + pub(crate) fn push(&mut self, instruction: Spanned) -> Result<(), CompileError> { + // Free read registers, and mark write registers. + // + // If a register is both read and written, it should be on both sides, so that we can verify + // that the register was in the right state beforehand. + let mut allocate = |read: &[RegId], write: &[RegId]| -> Result<(), CompileError> { + for reg in read { + self.free_register(*reg)?; + } + for reg in write { + self.mark_register(*reg)?; + } + Ok(()) + }; + + let allocate_result = match &instruction.item { + Instruction::Unreachable => Ok(()), + Instruction::LoadLiteral { dst, lit } => { + allocate(&[], &[*dst]).and( + // Free any registers on the literal + match lit { + Literal::Range { + start, + step, + end, + inclusion: _, + } => allocate(&[*start, *step, *end], &[]), + Literal::Bool(_) + | Literal::Int(_) + | Literal::Float(_) + | Literal::Filesize(_) + | Literal::Duration(_) + | Literal::Binary(_) + | Literal::Block(_) + | Literal::Closure(_) + | Literal::RowCondition(_) + | Literal::List { capacity: _ } + | Literal::Record { capacity: _ } + | Literal::Filepath { + val: _, + no_expand: _, + } + | Literal::Directory { + val: _, + no_expand: _, + } + | Literal::GlobPattern { + val: _, + no_expand: _, + } + | Literal::String(_) + | Literal::RawString(_) + | Literal::CellPath(_) + | Literal::Date(_) + | Literal::Nothing => Ok(()), + }, + ) + } + Instruction::LoadValue { dst, val: _ } => allocate(&[], &[*dst]), + Instruction::Move { dst, src } => allocate(&[*src], &[*dst]), + Instruction::Clone { dst, src } => allocate(&[*src], &[*dst, *src]), + Instruction::Collect { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::Span { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::Drop { src } => allocate(&[*src], &[]), + Instruction::Drain { src } => allocate(&[*src], &[]), + Instruction::DrainIfEnd { src } => allocate(&[*src], &[]), + Instruction::LoadVariable { dst, var_id: _ } => allocate(&[], &[*dst]), + Instruction::StoreVariable { var_id: _, src } => allocate(&[*src], &[]), + Instruction::DropVariable { var_id: _ } => Ok(()), + Instruction::LoadEnv { dst, key: _ } => allocate(&[], &[*dst]), + Instruction::LoadEnvOpt { dst, key: _ } => allocate(&[], &[*dst]), + Instruction::StoreEnv { key: _, src } => allocate(&[*src], &[]), + Instruction::PushPositional { src } => allocate(&[*src], &[]), + Instruction::AppendRest { src } => allocate(&[*src], &[]), + Instruction::PushFlag { name: _ } => Ok(()), + Instruction::PushShortFlag { short: _ } => Ok(()), + Instruction::PushNamed { name: _, src } => allocate(&[*src], &[]), + Instruction::PushShortNamed { short: _, src } => allocate(&[*src], &[]), + Instruction::PushParserInfo { name: _, info: _ } => Ok(()), + Instruction::RedirectOut { mode: _ } => Ok(()), + Instruction::RedirectErr { mode: _ } => Ok(()), + Instruction::CheckErrRedirected { src } => allocate(&[*src], &[*src]), + Instruction::OpenFile { + file_num: _, + path, + append: _, + } => allocate(&[*path], &[]), + Instruction::WriteFile { file_num: _, src } => allocate(&[*src], &[]), + Instruction::CloseFile { file_num: _ } => Ok(()), + Instruction::Call { + decl_id: _, + src_dst, + } => allocate(&[*src_dst], &[*src_dst]), + Instruction::StringAppend { src_dst, val } => allocate(&[*src_dst, *val], &[*src_dst]), + Instruction::GlobFrom { + src_dst, + no_expand: _, + } => allocate(&[*src_dst], &[*src_dst]), + Instruction::ListPush { src_dst, item } => allocate(&[*src_dst, *item], &[*src_dst]), + Instruction::ListSpread { src_dst, items } => { + allocate(&[*src_dst, *items], &[*src_dst]) + } + Instruction::RecordInsert { src_dst, key, val } => { + allocate(&[*src_dst, *key, *val], &[*src_dst]) + } + Instruction::RecordSpread { src_dst, items } => { + allocate(&[*src_dst, *items], &[*src_dst]) + } + Instruction::Not { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::BinaryOp { + lhs_dst, + op: _, + rhs, + } => allocate(&[*lhs_dst, *rhs], &[*lhs_dst]), + Instruction::FollowCellPath { src_dst, path } => { + allocate(&[*src_dst, *path], &[*src_dst]) + } + Instruction::CloneCellPath { dst, src, path } => { + allocate(&[*src, *path], &[*src, *dst]) + } + Instruction::UpsertCellPath { + src_dst, + path, + new_value, + } => allocate(&[*src_dst, *path, *new_value], &[*src_dst]), + Instruction::Jump { index: _ } => Ok(()), + Instruction::BranchIf { cond, index: _ } => allocate(&[*cond], &[]), + Instruction::BranchIfEmpty { src, index: _ } => allocate(&[*src], &[*src]), + Instruction::Match { + pattern: _, + src, + index: _, + } => allocate(&[*src], &[*src]), + Instruction::CheckMatchGuard { src } => allocate(&[*src], &[*src]), + Instruction::Iterate { + dst, + stream, + end_index: _, + } => allocate(&[*stream], &[*dst, *stream]), + Instruction::OnError { index: _ } => Ok(()), + Instruction::OnErrorInto { index: _, dst } => allocate(&[], &[*dst]), + Instruction::PopErrorHandler => Ok(()), + Instruction::ReturnEarly { src } => allocate(&[*src], &[]), + Instruction::Return { src } => allocate(&[*src], &[]), + }; + + // Add more context to the error + match allocate_result { + Ok(()) => (), + Err(CompileError::RegisterUninitialized { reg_id, caller }) => { + return Err(CompileError::RegisterUninitializedWhilePushingInstruction { + reg_id, + caller, + instruction: format!("{:?}", instruction.item), + span: instruction.span, + }); + } + Err(err) => return Err(err), + } + + self.instructions.push(instruction.item); + self.spans.push(instruction.span); + self.ast.push(None); + self.comments.push(String::new()); + Ok(()) + } + + /// Set the AST of the last instruction. Separate method because it's rarely used. + pub(crate) fn set_last_ast(&mut self, ast_ref: Option) { + *self.ast.last_mut().expect("no last instruction") = ast_ref; + } + + /// Add a comment to the last instruction. + pub(crate) fn add_comment(&mut self, comment: impl std::fmt::Display) { + add_comment( + self.comments.last_mut().expect("no last instruction"), + comment, + ) + } + + /// Load a register with a literal. + pub(crate) fn load_literal( + &mut self, + reg_id: RegId, + literal: Spanned, + ) -> Result<(), CompileError> { + self.push( + Instruction::LoadLiteral { + dst: reg_id, + lit: literal.item, + } + .into_spanned(literal.span), + )?; + Ok(()) + } + + /// Allocate a new register and load a literal into it. + pub(crate) fn literal(&mut self, literal: Spanned) -> Result { + let reg_id = self.next_register()?; + self.load_literal(reg_id, literal)?; + Ok(reg_id) + } + + /// Deallocate a register and set it to `Empty`, if it is allocated + pub(crate) fn drop_reg(&mut self, reg_id: RegId) -> Result<(), CompileError> { + if self.is_allocated(reg_id) { + self.push(Instruction::Drop { src: reg_id }.into_spanned(Span::unknown()))?; + } + Ok(()) + } + + /// Set a register to `Empty`, but mark it as in-use, e.g. for input + pub(crate) fn load_empty(&mut self, reg_id: RegId) -> Result<(), CompileError> { + self.drop_reg(reg_id)?; + self.mark_register(reg_id) + } + + /// Drain the stream in a register (fully consuming it) + pub(crate) fn drain(&mut self, src: RegId, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Drain { src }.into_spanned(span)) + } + + /// Add data to the `data` array and return a [`DataSlice`] referencing it. + pub(crate) fn data(&mut self, data: impl AsRef<[u8]>) -> Result { + let data = data.as_ref(); + let start = self.data.len(); + if data.is_empty() { + Ok(DataSlice::empty()) + } else if start + data.len() < u32::MAX as usize { + let slice = DataSlice { + start: start as u32, + len: data.len() as u32, + }; + self.data.extend_from_slice(data); + Ok(slice) + } else { + Err(CompileError::DataOverflow { + block_span: self.block_span, + }) + } + } + + /// Clone a register with a `clone` instruction. + pub(crate) fn clone_reg(&mut self, src: RegId, span: Span) -> Result { + let dst = self.next_register()?; + self.push(Instruction::Clone { dst, src }.into_spanned(span))?; + Ok(dst) + } + + /// Add a `branch-if` instruction + pub(crate) fn branch_if( + &mut self, + cond: RegId, + label_id: LabelId, + span: Span, + ) -> Result<(), CompileError> { + self.push( + Instruction::BranchIf { + cond, + index: label_id.0, + } + .into_spanned(span), + ) + } + + /// Add a `branch-if-empty` instruction + pub(crate) fn branch_if_empty( + &mut self, + src: RegId, + label_id: LabelId, + span: Span, + ) -> Result<(), CompileError> { + self.push( + Instruction::BranchIfEmpty { + src, + index: label_id.0, + } + .into_spanned(span), + ) + } + + /// Add a `jump` instruction + pub(crate) fn jump(&mut self, label_id: LabelId, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Jump { index: label_id.0 }.into_spanned(span)) + } + + /// Add a `match` instruction + pub(crate) fn r#match( + &mut self, + pattern: Pattern, + src: RegId, + label_id: LabelId, + span: Span, + ) -> Result<(), CompileError> { + self.push( + Instruction::Match { + pattern: Box::new(pattern), + src, + index: label_id.0, + } + .into_spanned(span), + ) + } + + /// The index that the next instruction [`.push()`](Self::push)ed will have. + pub(crate) fn here(&self) -> usize { + self.instructions.len() + } + + /// Allocate a new file number, for redirection. + pub(crate) fn next_file_num(&mut self) -> Result { + let next = self.file_count; + self.file_count = self + .file_count + .checked_add(1) + .ok_or(CompileError::FileOverflow { + block_span: self.block_span, + })?; + Ok(next) + } + + /// Push a new loop state onto the builder. Creates new labels that must be set. + pub(crate) fn begin_loop(&mut self) -> Loop { + let loop_ = Loop { + break_label: self.label(None), + continue_label: self.label(None), + }; + self.loop_stack.push(loop_); + loop_ + } + + /// True if we are currently in a loop. + pub(crate) fn is_in_loop(&self) -> bool { + !self.loop_stack.is_empty() + } + + /// Add a loop breaking jump instruction. + pub(crate) fn push_break(&mut self, span: Span) -> Result<(), CompileError> { + let loop_ = self + .loop_stack + .last() + .ok_or_else(|| CompileError::NotInALoop { + msg: "`break` called from outside of a loop".into(), + span: Some(span), + })?; + self.jump(loop_.break_label, span) + } + + /// Add a loop continuing jump instruction. + pub(crate) fn push_continue(&mut self, span: Span) -> Result<(), CompileError> { + let loop_ = self + .loop_stack + .last() + .ok_or_else(|| CompileError::NotInALoop { + msg: "`continue` called from outside of a loop".into(), + span: Some(span), + })?; + self.jump(loop_.continue_label, span) + } + + /// Pop the loop state. Checks that the loop being ended is the same one that was expected. + pub(crate) fn end_loop(&mut self, loop_: Loop) -> Result<(), CompileError> { + let ended_loop = self + .loop_stack + .pop() + .ok_or_else(|| CompileError::NotInALoop { + msg: "end_loop() called outside of a loop".into(), + span: None, + })?; + + if ended_loop == loop_ { + Ok(()) + } else { + Err(CompileError::IncoherentLoopState { + block_span: self.block_span, + }) + } + } + + /// Mark an unreachable code path. Produces an error at runtime if executed. + #[allow(dead_code)] // currently unused, but might be used in the future. + pub(crate) fn unreachable(&mut self, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Unreachable.into_spanned(span)) + } + + /// Consume the builder and produce the final [`IrBlock`]. + pub(crate) fn finish(mut self) -> Result { + // Add comments to label targets + for (index, label_target) in self.labels.iter().enumerate() { + if let Some(label_target) = label_target { + add_comment( + &mut self.comments[*label_target], + format_args!("label({index})"), + ); + } + } + + // Populate the actual target indices of labels into the instructions + for ((index, instruction), span) in + self.instructions.iter_mut().enumerate().zip(&self.spans) + { + if let Some(label_id) = instruction.branch_target() { + let target_index = self.labels.get(label_id).cloned().flatten().ok_or( + CompileError::UndefinedLabel { + label_id, + span: Some(*span), + }, + )?; + // Add a comment to the target index that we come from here + add_comment( + &mut self.comments[target_index], + format_args!("from({index}:)"), + ); + instruction.set_branch_target(target_index).map_err(|_| { + CompileError::SetBranchTargetOfNonBranchInstruction { + instruction: format!("{:?}", instruction), + span: *span, + } + })?; + } + } + + Ok(IrBlock { + instructions: self.instructions, + spans: self.spans, + data: self.data.into(), + ast: self.ast, + comments: self.comments.into_iter().map(|s| s.into()).collect(), + register_count: self + .register_allocation_state + .len() + .try_into() + .expect("register count overflowed in finish() despite previous checks"), + file_count: self.file_count, + }) + } +} + +/// Keeps track of the `break` and `continue` target labels for a loop. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Loop { + pub(crate) break_label: LabelId, + pub(crate) continue_label: LabelId, +} + +/// Add a new comment to an existing one +fn add_comment(comment: &mut String, new_comment: impl std::fmt::Display) { + use std::fmt::Write; + write!( + comment, + "{}{}", + if comment.is_empty() { "" } else { ", " }, + new_comment + ) + .expect("formatting failed"); +} diff --git a/nushell/crates/nu-engine/src/compile/call.rs b/nushell/crates/nu-engine/src/compile/call.rs new file mode 100644 index 0000000..3729489 --- /dev/null +++ b/nushell/crates/nu-engine/src/compile/call.rs @@ -0,0 +1,272 @@ +use std::sync::Arc; + +use nu_protocol::{ + IntoSpanned, RegId, Span, Spanned, + ast::{Argument, Call, Expression, ExternalArgument}, + engine::StateWorkingSet, + ir::{Instruction, IrAstRef, Literal}, +}; + +use super::{BlockBuilder, CompileError, RedirectModes, compile_expression, keyword::*}; + +pub(crate) fn compile_call( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + let decl = working_set.get_decl(call.decl_id); + + // Check if this call has --help - if so, just redirect to `help` + if call.named_iter().any(|(name, _, _)| name.item == "help") { + let name = working_set + .find_decl_name(call.decl_id) // check for name in scope + .and_then(|name| std::str::from_utf8(name).ok()) + .unwrap_or(decl.name()); // fall back to decl's name + return compile_help(working_set, builder, name.into_spanned(call.head), io_reg); + } + + // Try to figure out if this is a keyword call like `if`, and handle those specially + if decl.is_keyword() { + match decl.name() { + "if" => { + return compile_if(working_set, builder, call, redirect_modes, io_reg); + } + "match" => { + return compile_match(working_set, builder, call, redirect_modes, io_reg); + } + "const" => { + // This differs from the behavior of the const command, which adds the const value + // to the stack. Since `load-variable` also checks `engine_state` for the variable + // and will get a const value though, is it really necessary to do that? + return builder.load_empty(io_reg); + } + "alias" => { + // Alias does nothing + return builder.load_empty(io_reg); + } + "let" | "mut" => { + return compile_let(working_set, builder, call, redirect_modes, io_reg); + } + "try" => { + return compile_try(working_set, builder, call, redirect_modes, io_reg); + } + "loop" => { + return compile_loop(working_set, builder, call, redirect_modes, io_reg); + } + "while" => { + return compile_while(working_set, builder, call, redirect_modes, io_reg); + } + "for" => { + return compile_for(working_set, builder, call, redirect_modes, io_reg); + } + "break" => { + return compile_break(working_set, builder, call, redirect_modes, io_reg); + } + "continue" => { + return compile_continue(working_set, builder, call, redirect_modes, io_reg); + } + "return" => { + return compile_return(working_set, builder, call, redirect_modes, io_reg); + } + "def" | "export def" => { + return builder.load_empty(io_reg); + } + _ => (), + } + } + + // Keep AST if the decl needs it. + let requires_ast = decl.requires_ast_for_arguments(); + + // It's important that we evaluate the args first before trying to set up the argument + // state for the call. + // + // We could technically compile anything that isn't another call safely without worrying about + // the argument state, but we'd have to check all of that first and it just isn't really worth + // it. + enum CompiledArg<'a> { + Positional(RegId, Span, Option), + Named( + &'a str, + Option<&'a str>, + Option, + Span, + Option, + ), + Spread(RegId, Span, Option), + } + + let mut compiled_args = vec![]; + + for arg in &call.arguments { + let arg_reg = arg + .expr() + .map(|expr| { + let arg_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + expr, + RedirectModes::value(arg.span()), + None, + arg_reg, + )?; + + Ok(arg_reg) + }) + .transpose()?; + + let ast_ref = arg + .expr() + .filter(|_| requires_ast) + .map(|expr| IrAstRef(Arc::new(expr.clone()))); + + match arg { + Argument::Positional(_) | Argument::Unknown(_) => { + compiled_args.push(CompiledArg::Positional( + arg_reg.expect("expr() None in non-Named"), + arg.span(), + ast_ref, + )) + } + Argument::Named((name, short, _)) => compiled_args.push(CompiledArg::Named( + &name.item, + short.as_ref().map(|spanned| spanned.item.as_str()), + arg_reg, + arg.span(), + ast_ref, + )), + Argument::Spread(_) => compiled_args.push(CompiledArg::Spread( + arg_reg.expect("expr() None in non-Named"), + arg.span(), + ast_ref, + )), + } + } + + // Now that the args are all compiled, set up the call state (argument stack and redirections) + for arg in compiled_args { + match arg { + CompiledArg::Positional(reg, span, ast_ref) => { + builder.push(Instruction::PushPositional { src: reg }.into_spanned(span))?; + builder.set_last_ast(ast_ref); + } + CompiledArg::Named(name, short, Some(reg), span, ast_ref) => { + if !name.is_empty() { + let name = builder.data(name)?; + builder.push(Instruction::PushNamed { name, src: reg }.into_spanned(span))?; + } else { + let short = builder.data(short.unwrap_or(""))?; + builder + .push(Instruction::PushShortNamed { short, src: reg }.into_spanned(span))?; + } + builder.set_last_ast(ast_ref); + } + CompiledArg::Named(name, short, None, span, ast_ref) => { + if !name.is_empty() { + let name = builder.data(name)?; + builder.push(Instruction::PushFlag { name }.into_spanned(span))?; + } else { + let short = builder.data(short.unwrap_or(""))?; + builder.push(Instruction::PushShortFlag { short }.into_spanned(span))?; + } + builder.set_last_ast(ast_ref); + } + CompiledArg::Spread(reg, span, ast_ref) => { + builder.push(Instruction::AppendRest { src: reg }.into_spanned(span))?; + builder.set_last_ast(ast_ref); + } + } + } + + // Add any parser info from the call + for (name, info) in &call.parser_info { + let name = builder.data(name)?; + let info = Box::new(info.clone()); + builder.push(Instruction::PushParserInfo { name, info }.into_spanned(call.head))?; + } + + if let Some(mode) = redirect_modes.out { + builder.push(mode.map(|mode| Instruction::RedirectOut { mode }))?; + } + + if let Some(mode) = redirect_modes.err { + builder.push(mode.map(|mode| Instruction::RedirectErr { mode }))?; + } + + // The state is set up, so we can do the call into io_reg + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + + Ok(()) +} + +pub(crate) fn compile_help( + working_set: &StateWorkingSet<'_>, + builder: &mut BlockBuilder, + decl_name: Spanned<&str>, + io_reg: RegId, +) -> Result<(), CompileError> { + let help_command_id = + working_set + .find_decl(b"help") + .ok_or_else(|| CompileError::MissingRequiredDeclaration { + decl_name: "help".into(), + span: decl_name.span, + })?; + + let name_data = builder.data(decl_name.item)?; + let name_literal = builder.literal(decl_name.map(|_| Literal::String(name_data)))?; + + builder.push(Instruction::PushPositional { src: name_literal }.into_spanned(decl_name.span))?; + + builder.push( + Instruction::Call { + decl_id: help_command_id, + src_dst: io_reg, + } + .into_spanned(decl_name.span), + )?; + + Ok(()) +} + +pub(crate) fn compile_external_call( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + head: &Expression, + args: &[ExternalArgument], + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pass everything to run-external + let run_external_id = working_set + .find_decl(b"run-external") + .ok_or(CompileError::RunExternalNotFound { span: head.span })?; + + let mut call = Call::new(head.span); + call.decl_id = run_external_id; + + call.arguments.push(Argument::Positional(head.clone())); + + for arg in args { + match arg { + ExternalArgument::Regular(expr) => { + call.arguments.push(Argument::Positional(expr.clone())); + } + ExternalArgument::Spread(expr) => { + call.arguments.push(Argument::Spread(expr.clone())); + } + } + } + + compile_call(working_set, builder, &call, redirect_modes, io_reg) +} diff --git a/nushell/crates/nu-engine/src/compile/expression.rs b/nushell/crates/nu-engine/src/compile/expression.rs new file mode 100644 index 0000000..3fcbbac --- /dev/null +++ b/nushell/crates/nu-engine/src/compile/expression.rs @@ -0,0 +1,572 @@ +use super::{ + BlockBuilder, CompileError, RedirectModes, compile_binary_op, compile_block, compile_call, + compile_external_call, compile_load_env, +}; + +use nu_protocol::{ + ENV_VARIABLE_ID, IntoSpanned, RegId, Span, Value, + ast::{CellPath, Expr, Expression, ListItem, RecordItem, ValueWithUnit}, + engine::StateWorkingSet, + ir::{DataSlice, Instruction, Literal}, +}; + +pub(crate) fn compile_expression( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + expr: &Expression, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let drop_input = |builder: &mut BlockBuilder| { + if let Some(in_reg) = in_reg { + if in_reg != out_reg { + builder.drop_reg(in_reg)?; + } + } + Ok(()) + }; + + let lit = |builder: &mut BlockBuilder, literal: Literal| { + drop_input(builder)?; + + builder + .push( + Instruction::LoadLiteral { + dst: out_reg, + lit: literal, + } + .into_spanned(expr.span), + ) + .map(|_| ()) + }; + + let ignore = |builder: &mut BlockBuilder| { + drop_input(builder)?; + builder.load_empty(out_reg) + }; + + let unexpected = |expr_name: &str| CompileError::UnexpectedExpression { + expr_name: expr_name.into(), + span: expr.span, + }; + + let move_in_reg_to_out_reg = |builder: &mut BlockBuilder| { + // Ensure that out_reg contains the input value, because a call only uses one register + if let Some(in_reg) = in_reg { + if in_reg != out_reg { + // Have to move in_reg to out_reg so it can be used + builder.push( + Instruction::Move { + dst: out_reg, + src: in_reg, + } + .into_spanned(expr.span), + )?; + } + } else { + // Will have to initialize out_reg with Empty first + builder.load_empty(out_reg)?; + } + Ok(()) + }; + + match &expr.expr { + Expr::AttributeBlock(ab) => compile_expression( + working_set, + builder, + &ab.item, + redirect_modes, + in_reg, + out_reg, + ), + Expr::Bool(b) => lit(builder, Literal::Bool(*b)), + Expr::Int(i) => lit(builder, Literal::Int(*i)), + Expr::Float(f) => lit(builder, Literal::Float(*f)), + Expr::Binary(bin) => { + let data_slice = builder.data(bin)?; + lit(builder, Literal::Binary(data_slice)) + } + Expr::Range(range) => { + // Compile the subexpressions of the range + let compile_part = |builder: &mut BlockBuilder, + part_expr: Option<&Expression>| + -> Result { + let reg = builder.next_register()?; + if let Some(part_expr) = part_expr { + compile_expression( + working_set, + builder, + part_expr, + RedirectModes::value(part_expr.span), + None, + reg, + )?; + } else { + builder.load_literal(reg, Literal::Nothing.into_spanned(expr.span))?; + } + Ok(reg) + }; + + drop_input(builder)?; + + let start = compile_part(builder, range.from.as_ref())?; + let step = compile_part(builder, range.next.as_ref())?; + let end = compile_part(builder, range.to.as_ref())?; + + // Assemble the range + builder.load_literal( + out_reg, + Literal::Range { + start, + step, + end, + inclusion: range.operator.inclusion, + } + .into_spanned(expr.span), + ) + } + Expr::Var(var_id) => { + drop_input(builder)?; + builder.push( + Instruction::LoadVariable { + dst: out_reg, + var_id: *var_id, + } + .into_spanned(expr.span), + )?; + Ok(()) + } + Expr::VarDecl(_) => Err(unexpected("VarDecl")), + Expr::Call(call) => { + move_in_reg_to_out_reg(builder)?; + + compile_call(working_set, builder, call, redirect_modes, out_reg) + } + Expr::ExternalCall(head, args) => { + move_in_reg_to_out_reg(builder)?; + + compile_external_call(working_set, builder, head, args, redirect_modes, out_reg) + } + Expr::Operator(_) => Err(unexpected("Operator")), + Expr::RowCondition(block_id) => lit(builder, Literal::RowCondition(*block_id)), + Expr::UnaryNot(subexpr) => { + drop_input(builder)?; + compile_expression( + working_set, + builder, + subexpr, + RedirectModes::value(subexpr.span), + None, + out_reg, + )?; + builder.push(Instruction::Not { src_dst: out_reg }.into_spanned(expr.span))?; + Ok(()) + } + Expr::BinaryOp(lhs, op, rhs) => { + if let Expr::Operator(operator) = op.expr { + drop_input(builder)?; + compile_binary_op( + working_set, + builder, + lhs, + operator.into_spanned(op.span), + rhs, + expr.span, + out_reg, + ) + } else { + Err(CompileError::UnsupportedOperatorExpression { span: op.span }) + } + } + Expr::Collect(var_id, expr) => { + let store_reg = if let Some(in_reg) = in_reg { + // Collect, clone, store + builder.push(Instruction::Collect { src_dst: in_reg }.into_spanned(expr.span))?; + builder.clone_reg(in_reg, expr.span)? + } else { + // Just store nothing in the variable + builder.literal(Literal::Nothing.into_spanned(Span::unknown()))? + }; + builder.push( + Instruction::StoreVariable { + var_id: *var_id, + src: store_reg, + } + .into_spanned(expr.span), + )?; + compile_expression(working_set, builder, expr, redirect_modes, in_reg, out_reg)?; + // Clean it up afterward + builder.push(Instruction::DropVariable { var_id: *var_id }.into_spanned(expr.span))?; + Ok(()) + } + Expr::Subexpression(block_id) => { + let block = working_set.get_block(*block_id); + compile_block(working_set, builder, block, redirect_modes, in_reg, out_reg) + } + Expr::Block(block_id) => lit(builder, Literal::Block(*block_id)), + Expr::Closure(block_id) => lit(builder, Literal::Closure(*block_id)), + Expr::MatchBlock(_) => Err(unexpected("MatchBlock")), // only for `match` keyword + Expr::List(items) => { + // Guess capacity based on items (does not consider spread as more than 1) + lit( + builder, + Literal::List { + capacity: items.len(), + }, + )?; + for item in items { + // Compile the expression of the item / spread + let reg = builder.next_register()?; + let expr = match item { + ListItem::Item(expr) | ListItem::Spread(_, expr) => expr, + }; + compile_expression( + working_set, + builder, + expr, + RedirectModes::value(expr.span), + None, + reg, + )?; + + match item { + ListItem::Item(_) => { + // Add each item using list-push + builder.push( + Instruction::ListPush { + src_dst: out_reg, + item: reg, + } + .into_spanned(expr.span), + )?; + } + ListItem::Spread(spread_span, _) => { + // Spread the list using list-spread + builder.push( + Instruction::ListSpread { + src_dst: out_reg, + items: reg, + } + .into_spanned(*spread_span), + )?; + } + } + } + Ok(()) + } + Expr::Table(table) => { + lit( + builder, + Literal::List { + capacity: table.rows.len(), + }, + )?; + + // Evaluate the columns + let column_registers = table + .columns + .iter() + .map(|column| { + let reg = builder.next_register()?; + compile_expression( + working_set, + builder, + column, + RedirectModes::value(column.span), + None, + reg, + )?; + Ok(reg) + }) + .collect::, CompileError>>()?; + + // Build records for each row + for row in table.rows.iter() { + let row_reg = builder.next_register()?; + builder.load_literal( + row_reg, + Literal::Record { + capacity: table.columns.len(), + } + .into_spanned(expr.span), + )?; + for (column_reg, item) in column_registers.iter().zip(row.iter()) { + let column_reg = builder.clone_reg(*column_reg, item.span)?; + let item_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + item, + RedirectModes::value(item.span), + None, + item_reg, + )?; + builder.push( + Instruction::RecordInsert { + src_dst: row_reg, + key: column_reg, + val: item_reg, + } + .into_spanned(item.span), + )?; + } + builder.push( + Instruction::ListPush { + src_dst: out_reg, + item: row_reg, + } + .into_spanned(expr.span), + )?; + } + + // Free the column registers, since they aren't needed anymore + for reg in column_registers { + builder.drop_reg(reg)?; + } + + Ok(()) + } + Expr::Record(items) => { + lit( + builder, + Literal::Record { + capacity: items.len(), + }, + )?; + + for item in items { + match item { + RecordItem::Pair(key, val) => { + // Add each item using record-insert + let key_reg = builder.next_register()?; + let val_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + key, + RedirectModes::value(key.span), + None, + key_reg, + )?; + compile_expression( + working_set, + builder, + val, + RedirectModes::value(val.span), + None, + val_reg, + )?; + builder.push( + Instruction::RecordInsert { + src_dst: out_reg, + key: key_reg, + val: val_reg, + } + .into_spanned(expr.span), + )?; + } + RecordItem::Spread(spread_span, expr) => { + // Spread the expression using record-spread + let reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::value(expr.span), + None, + reg, + )?; + builder.push( + Instruction::RecordSpread { + src_dst: out_reg, + items: reg, + } + .into_spanned(*spread_span), + )?; + } + } + } + Ok(()) + } + Expr::Keyword(kw) => { + // keyword: just pass through expr, since commands that use it and are not being + // specially handled already are often just positional anyway + compile_expression( + working_set, + builder, + &kw.expr, + redirect_modes, + in_reg, + out_reg, + ) + } + Expr::ValueWithUnit(value_with_unit) => { + lit(builder, literal_from_value_with_unit(value_with_unit)?) + } + Expr::DateTime(dt) => lit(builder, Literal::Date(Box::new(*dt))), + Expr::Filepath(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::Filepath { + val, + no_expand: *no_expand, + }, + ) + } + Expr::Directory(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::Directory { + val, + no_expand: *no_expand, + }, + ) + } + Expr::GlobPattern(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::GlobPattern { + val, + no_expand: *no_expand, + }, + ) + } + Expr::String(s) => { + let data_slice = builder.data(s)?; + lit(builder, Literal::String(data_slice)) + } + Expr::RawString(rs) => { + let data_slice = builder.data(rs)?; + lit(builder, Literal::RawString(data_slice)) + } + Expr::CellPath(path) => lit(builder, Literal::CellPath(Box::new(path.clone()))), + Expr::FullCellPath(full_cell_path) => { + if matches!(full_cell_path.head.expr, Expr::Var(ENV_VARIABLE_ID)) { + compile_load_env(builder, expr.span, &full_cell_path.tail, out_reg) + } else { + compile_expression( + working_set, + builder, + &full_cell_path.head, + // Only capture the output if there is a tail. This was a bit of a headscratcher + // as the parser emits a FullCellPath with no tail for subexpressions in + // general, which shouldn't be captured any differently than they otherwise + // would be. + if !full_cell_path.tail.is_empty() { + RedirectModes::value(expr.span) + } else { + redirect_modes + }, + in_reg, + out_reg, + )?; + // Only do the follow if this is actually needed + if !full_cell_path.tail.is_empty() { + let cell_path_reg = builder.literal( + Literal::CellPath(Box::new(CellPath { + members: full_cell_path.tail.clone(), + })) + .into_spanned(expr.span), + )?; + builder.push( + Instruction::FollowCellPath { + src_dst: out_reg, + path: cell_path_reg, + } + .into_spanned(expr.span), + )?; + } + Ok(()) + } + } + Expr::ImportPattern(_) => Err(unexpected("ImportPattern")), + Expr::Overlay(_) => Err(unexpected("Overlay")), + Expr::Signature(_) => ignore(builder), // no effect + Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => { + let mut exprs_iter = exprs.iter().peekable(); + + if exprs_iter + .peek() + .is_some_and(|e| matches!(e.expr, Expr::String(..) | Expr::RawString(..))) + { + // If the first expression is a string or raw string literal, just take it and build + // from that + compile_expression( + working_set, + builder, + exprs_iter.next().expect("peek() was Some"), + RedirectModes::value(expr.span), + None, + out_reg, + )?; + } else { + // Start with an empty string + lit(builder, Literal::String(DataSlice::empty()))?; + } + + // Compile each expression and append to out_reg + for expr in exprs_iter { + let scratch_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::value(expr.span), + None, + scratch_reg, + )?; + builder.push( + Instruction::StringAppend { + src_dst: out_reg, + val: scratch_reg, + } + .into_spanned(expr.span), + )?; + } + + // If it's a glob interpolation, change it to a glob + if let Expr::GlobInterpolation(_, no_expand) = expr.expr { + builder.push( + Instruction::GlobFrom { + src_dst: out_reg, + no_expand, + } + .into_spanned(expr.span), + )?; + } + + Ok(()) + } + Expr::Nothing => lit(builder, Literal::Nothing), + Expr::Garbage => Err(CompileError::Garbage { span: expr.span }), + } +} + +fn literal_from_value_with_unit(value_with_unit: &ValueWithUnit) -> Result { + let Expr::Int(int_value) = value_with_unit.expr.expr else { + return Err(CompileError::UnexpectedExpression { + expr_name: format!("{:?}", value_with_unit.expr), + span: value_with_unit.expr.span, + }); + }; + + match value_with_unit + .unit + .item + .build_value(int_value, Span::unknown()) + .map_err(|err| CompileError::InvalidLiteral { + msg: err.to_string(), + span: value_with_unit.expr.span, + })? { + Value::Filesize { val, .. } => Ok(Literal::Filesize(val)), + Value::Duration { val, .. } => Ok(Literal::Duration(val)), + other => Err(CompileError::InvalidLiteral { + msg: format!("bad value returned by Unit::build_value(): {other:?}"), + span: value_with_unit.unit.span, + }), + } +} diff --git a/nushell/crates/nu-engine/src/compile/keyword.rs b/nushell/crates/nu-engine/src/compile/keyword.rs new file mode 100644 index 0000000..971b8c7 --- /dev/null +++ b/nushell/crates/nu-engine/src/compile/keyword.rs @@ -0,0 +1,884 @@ +use nu_protocol::{ + IntoSpanned, RegId, Type, VarId, + ast::{Block, Call, Expr, Expression}, + engine::StateWorkingSet, + ir::Instruction, +}; + +use super::{BlockBuilder, CompileError, RedirectModes, compile_block, compile_expression}; + +/// Compile a call to `if` as a branch-if +pub(crate) fn compile_if( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- + // not %io_reg + // branch-if %io_reg, FALSE + // TRUE: ...... + // jump END + // FALSE: ...... OR drop %io_reg + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "if".into(), + span: call.head, + }; + + let condition = call.positional_nth(0).ok_or_else(invalid)?; + let true_block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let else_arg = call.positional_nth(2); + + let true_block_id = true_block_arg.as_block().ok_or_else(invalid)?; + let true_block = working_set.get_block(true_block_id); + + let true_label = builder.label(None); + let false_label = builder.label(None); + let end_label = builder.label(None); + + let not_condition_reg = { + // Compile the condition first + let condition_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + condition, + RedirectModes::value(condition.span), + None, + condition_reg, + )?; + + // Negate the condition - we basically only want to jump if the condition is false + builder.push( + Instruction::Not { + src_dst: condition_reg, + } + .into_spanned(call.head), + )?; + + condition_reg + }; + + // Set up a branch if the condition is false. + builder.branch_if(not_condition_reg, false_label, call.head)?; + builder.add_comment("if false"); + + // Compile the true case + builder.set_label(true_label, builder.here())?; + compile_block( + working_set, + builder, + true_block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + + // Add a jump over the false case + builder.jump(end_label, else_arg.map(|e| e.span).unwrap_or(call.head))?; + builder.add_comment("end if"); + + // On the else side now, assert that io_reg is still valid + builder.set_label(false_label, builder.here())?; + builder.mark_register(io_reg)?; + + if let Some(else_arg) = else_arg { + let Expression { + expr: Expr::Keyword(else_keyword), + .. + } = else_arg + else { + return Err(invalid()); + }; + + if else_keyword.keyword.as_ref() != b"else" { + return Err(invalid()); + } + + let else_expr = &else_keyword.expr; + + match &else_expr.expr { + Expr::Block(block_id) => { + let false_block = working_set.get_block(*block_id); + compile_block( + working_set, + builder, + false_block, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + _ => { + // The else case supports bare expressions too, not only blocks + compile_expression( + working_set, + builder, + else_expr, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + } + } else { + // We don't have an else expression/block, so just set io_reg = Empty + builder.load_empty(io_reg)?; + } + + // Set the end label + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `match` +pub(crate) fn compile_match( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %match_reg <- + // collect %match_reg + // match (pat1), %match_reg, PAT1 + // MATCH2: match (pat2), %match_reg, PAT2 + // FAIL: drop %io_reg + // drop %match_reg + // jump END + // PAT1: %guard_reg <- + // check-match-guard %guard_reg + // not %guard_reg + // branch-if %guard_reg, MATCH2 + // drop %match_reg + // <...expr...> + // jump END + // PAT2: drop %match_reg + // <...expr...> + // jump END + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "match".into(), + span: call.head, + }; + + let match_expr = call.positional_nth(0).ok_or_else(invalid)?; + + let match_block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let match_block = match_block_arg.as_match_block().ok_or_else(invalid)?; + + let match_reg = builder.next_register()?; + + // Evaluate the match expression (patterns will be checked against this). + compile_expression( + working_set, + builder, + match_expr, + RedirectModes::value(match_expr.span), + None, + match_reg, + )?; + + // Important to collect it first + builder.push(Instruction::Collect { src_dst: match_reg }.into_spanned(match_expr.span))?; + + // Generate the `match` instructions. Guards are not used at this stage. + let mut match_labels = Vec::with_capacity(match_block.len()); + let mut next_labels = Vec::with_capacity(match_block.len()); + let end_label = builder.label(None); + + for (pattern, _) in match_block { + let match_label = builder.label(None); + match_labels.push(match_label); + builder.r#match( + pattern.pattern.clone(), + match_reg, + match_label, + pattern.span, + )?; + // Also add a label for the next match instruction or failure case + next_labels.push(builder.label(Some(builder.here()))); + } + + // Match fall-through to jump to the end, if no match + builder.load_empty(io_reg)?; + builder.drop_reg(match_reg)?; + builder.jump(end_label, call.head)?; + + // Generate each of the match expressions. Handle guards here, if present. + for (index, (pattern, expr)) in match_block.iter().enumerate() { + let match_label = match_labels[index]; + let next_label = next_labels[index]; + + // `io_reg` and `match_reg` are still valid at each of these branch targets + builder.mark_register(io_reg)?; + builder.mark_register(match_reg)?; + + // Set the original match instruction target here + builder.set_label(match_label, builder.here())?; + + // Handle guard, if present + if let Some(guard) = &pattern.guard { + let guard_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + guard, + RedirectModes::value(guard.span), + None, + guard_reg, + )?; + builder + .push(Instruction::CheckMatchGuard { src: guard_reg }.into_spanned(guard.span))?; + builder.push(Instruction::Not { src_dst: guard_reg }.into_spanned(guard.span))?; + // Branch to the next match instruction if the branch fails to match + builder.branch_if( + guard_reg, + next_label, + // Span the branch with the next pattern, or the head if this is the end + match_block + .get(index + 1) + .map(|b| b.0.span) + .unwrap_or(call.head), + )?; + builder.add_comment("if match guard false"); + } + + // match_reg no longer needed, successful match + builder.drop_reg(match_reg)?; + + // Execute match right hand side expression + if let Expr::Block(block_id) = expr.expr { + let block = working_set.get_block(block_id); + compile_block( + working_set, + builder, + block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + } else { + compile_expression( + working_set, + builder, + expr, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + } + + // Jump to the end after the match logic is done + builder.jump(end_label, call.head)?; + builder.add_comment("end match"); + } + + // Set the end destination + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `let` or `mut` (just do store-variable) +pub(crate) fn compile_let( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- ...... <- %io_reg + // store-variable $var, %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "let".into(), + span: call.head, + }; + + let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_arg = call.positional_nth(1).ok_or_else(invalid)?; + + let var_id = var_decl_arg.as_var().ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let variable = working_set.get_variable(var_id); + + compile_block( + working_set, + builder, + block, + RedirectModes::value(call.head), + Some(io_reg), + io_reg, + )?; + + // If the variable is a glob type variable, we should cast it with GlobFrom + if variable.ty == Type::Glob { + builder.push( + Instruction::GlobFrom { + src_dst: io_reg, + no_expand: true, + } + .into_spanned(call.head), + )?; + } + + builder.push( + Instruction::StoreVariable { + var_id, + src: io_reg, + } + .into_spanned(call.head), + )?; + builder.add_comment("let"); + + // Don't forget to set io_reg to Empty afterward, as that's the result of an assignment + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `try`, setting an error handler over the evaluated block +pub(crate) fn compile_try( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode (literal block): + // + // on-error-into ERR, %io_reg // or without + // %io_reg <- <...block...> <- %io_reg + // write-to-out-dests %io_reg + // pop-error-handler + // jump END + // ERR: clone %err_reg, %io_reg + // store-variable $err_var, %err_reg // or without + // %io_reg <- <...catch block...> <- %io_reg // set to empty if no catch block + // END: + // + // with expression that can't be inlined: + // + // %closure_reg <- + // on-error-into ERR, %io_reg + // %io_reg <- <...block...> <- %io_reg + // write-to-out-dests %io_reg + // pop-error-handler + // jump END + // ERR: clone %err_reg, %io_reg + // push-positional %closure_reg + // push-positional %err_reg + // call "do", %io_reg + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "try".into(), + span: call.head, + }; + + let block_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let catch_expr = match call.positional_nth(1) { + Some(kw_expr) => Some(kw_expr.as_keyword().ok_or_else(invalid)?), + None => None, + }; + let catch_span = catch_expr.map(|e| e.span).unwrap_or(call.head); + + let err_label = builder.label(None); + let end_label = builder.label(None); + + // We have two ways of executing `catch`: if it was provided as a literal, we can inline it. + // Otherwise, we have to evaluate the expression and keep it as a register, and then call `do`. + enum CatchType<'a> { + Block { + block: &'a Block, + var_id: Option, + }, + Closure { + closure_reg: RegId, + }, + } + + let catch_type = catch_expr + .map(|catch_expr| match catch_expr.as_block() { + Some(block_id) => { + let block = working_set.get_block(block_id); + let var_id = block.signature.get_positional(0).and_then(|v| v.var_id); + Ok(CatchType::Block { block, var_id }) + } + None => { + // We have to compile the catch_expr and use it as a closure + let closure_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + catch_expr, + RedirectModes::value(catch_expr.span), + None, + closure_reg, + )?; + Ok(CatchType::Closure { closure_reg }) + } + }) + .transpose()?; + + // Put the error handler instruction. If we have a catch expression then we should capture the + // error. + if catch_type.is_some() { + builder.push( + Instruction::OnErrorInto { + index: err_label.0, + dst: io_reg, + } + .into_spanned(call.head), + )? + } else { + // Otherwise, we don't need the error value. + builder.push(Instruction::OnError { index: err_label.0 }.into_spanned(call.head))? + }; + + builder.add_comment("try"); + + // Compile the block + compile_block( + working_set, + builder, + block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + + // Successful case: + // - write to the current output destinations + // - pop the error handler + if let Some(mode) = redirect_modes.out { + builder.push(mode.map(|mode| Instruction::RedirectOut { mode }))?; + } + + if let Some(mode) = redirect_modes.err { + builder.push(mode.map(|mode| Instruction::RedirectErr { mode }))?; + } + builder.push(Instruction::DrainIfEnd { src: io_reg }.into_spanned(call.head))?; + builder.push(Instruction::PopErrorHandler.into_spanned(call.head))?; + + // Jump over the failure case + builder.jump(end_label, catch_span)?; + + // This is the error handler + builder.set_label(err_label, builder.here())?; + + // Mark out register as likely not clean - state in error handler is not well defined + builder.mark_register(io_reg)?; + + // Now compile whatever is necessary for the error handler + match catch_type { + Some(CatchType::Block { block, var_id }) => { + // Error will be in io_reg + builder.mark_register(io_reg)?; + if let Some(var_id) = var_id { + // Take a copy of the error as $err, since it will also be input + let err_reg = builder.next_register()?; + builder.push( + Instruction::Clone { + dst: err_reg, + src: io_reg, + } + .into_spanned(catch_span), + )?; + builder.push( + Instruction::StoreVariable { + var_id, + src: err_reg, + } + .into_spanned(catch_span), + )?; + } + // Compile the block, now that the variable is set + compile_block( + working_set, + builder, + block, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + Some(CatchType::Closure { closure_reg }) => { + // We should call `do`. Error will be in io_reg + let do_decl_id = working_set.find_decl(b"do").ok_or_else(|| { + CompileError::MissingRequiredDeclaration { + decl_name: "do".into(), + span: call.head, + } + })?; + + // Take a copy of io_reg, because we pass it both as an argument and input + builder.mark_register(io_reg)?; + let err_reg = builder.next_register()?; + builder.push( + Instruction::Clone { + dst: err_reg, + src: io_reg, + } + .into_spanned(catch_span), + )?; + + // Push the closure and the error + builder + .push(Instruction::PushPositional { src: closure_reg }.into_spanned(catch_span))?; + builder.push(Instruction::PushPositional { src: err_reg }.into_spanned(catch_span))?; + + // Call `$err | do $closure $err` + builder.push( + Instruction::Call { + decl_id: do_decl_id, + src_dst: io_reg, + } + .into_spanned(catch_span), + )?; + } + None => { + // Just set out to empty. + builder.load_empty(io_reg)?; + } + } + + // This is the end - if we succeeded, should jump here + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `loop` (via `jump`) +pub(crate) fn compile_loop( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // drop %io_reg + // LOOP: %io_reg <- ...... + // drain %io_reg + // jump %LOOP + // END: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "loop".into(), + span: call.head, + }; + + let block_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let loop_ = builder.begin_loop(); + builder.load_empty(io_reg)?; + + builder.set_label(loop_.continue_label, builder.here())?; + + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the output, just like for a semicolon + builder.drain(io_reg, call.head)?; + + builder.jump(loop_.continue_label, call.head)?; + builder.add_comment("loop"); + + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // State of %io_reg is not necessarily well defined here due to control flow, so make sure it's + // empty. + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `while`, via branch instructions +pub(crate) fn compile_while( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // LOOP: %io_reg <- + // branch-if %io_reg, TRUE + // jump FALSE + // TRUE: %io_reg <- ...... + // drain %io_reg + // jump LOOP + // FALSE: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "while".into(), + span: call.head, + }; + + let cond_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let loop_ = builder.begin_loop(); + builder.set_label(loop_.continue_label, builder.here())?; + + let true_label = builder.label(None); + + compile_expression( + working_set, + builder, + cond_arg, + RedirectModes::value(call.head), + None, + io_reg, + )?; + + builder.branch_if(io_reg, true_label, call.head)?; + builder.add_comment("while"); + builder.jump(loop_.break_label, call.head)?; + builder.add_comment("end while"); + + builder.set_label(true_label, builder.here())?; + + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the result, just like for a semicolon + builder.drain(io_reg, call.head)?; + + builder.jump(loop_.continue_label, call.head)?; + builder.add_comment("while"); + + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // State of %io_reg is not necessarily well defined here due to control flow, so make sure it's + // empty. + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `for` (via `iterate`) +pub(crate) fn compile_for( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %stream_reg <- + // LOOP: iterate %io_reg, %stream_reg, END + // store-variable $var, %io_reg + // %io_reg <- <...block...> + // drain %io_reg + // jump LOOP + // END: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "for".into(), + span: call.head, + }; + + if call.get_named_arg("numbered").is_some() { + // This is deprecated and we don't support it. + return Err(invalid()); + } + + let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?; + let var_id = var_decl_arg.as_var().ok_or_else(invalid)?; + + let in_arg = call.positional_nth(1).ok_or_else(invalid)?; + let in_expr = in_arg.as_keyword().ok_or_else(invalid)?; + + let block_arg = call.positional_nth(2).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + // Ensure io_reg is marked so we don't use it + builder.mark_register(io_reg)?; + + let stream_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + in_expr, + RedirectModes::value(in_expr.span), + None, + stream_reg, + )?; + + // Set up loop state + let loop_ = builder.begin_loop(); + builder.set_label(loop_.continue_label, builder.here())?; + + // This gets a value from the stream each time it's executed + // io_reg basically will act as our scratch register here + builder.push( + Instruction::Iterate { + dst: io_reg, + stream: stream_reg, + end_index: loop_.break_label.0, + } + .into_spanned(call.head), + )?; + builder.add_comment("for"); + + // Put the received value in the variable + builder.push( + Instruction::StoreVariable { + var_id, + src: io_reg, + } + .into_spanned(var_decl_arg.span), + )?; + + // Do the body of the block + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the output, just like for a semicolon + builder.drain(io_reg, call.head)?; + + // Loop back to iterate to get the next value + builder.jump(loop_.continue_label, call.head)?; + + // Set the end of the loop + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // We don't need stream_reg anymore, after the loop + // io_reg may or may not be empty, so be sure it is + builder.free_register(stream_reg)?; + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `break`. +pub(crate) fn compile_break( + _working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + if builder.is_in_loop() { + builder.load_empty(io_reg)?; + builder.push_break(call.head)?; + builder.add_comment("break"); + } else { + // Fall back to calling the command if we can't find the loop target statically + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + } + Ok(()) +} + +/// Compile a call to `continue`. +pub(crate) fn compile_continue( + _working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + if builder.is_in_loop() { + builder.load_empty(io_reg)?; + builder.push_continue(call.head)?; + builder.add_comment("continue"); + } else { + // Fall back to calling the command if we can't find the loop target statically + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + } + Ok(()) +} + +/// Compile a call to `return` as a `return-early` instruction. +/// +/// This is not strictly necessary, but it is more efficient. +pub(crate) fn compile_return( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- + // return-early %io_reg + if let Some(arg_expr) = call.positional_nth(0) { + compile_expression( + working_set, + builder, + arg_expr, + RedirectModes::value(arg_expr.span), + None, + io_reg, + )?; + } else { + builder.load_empty(io_reg)?; + } + + // TODO: It would be nice if this could be `return` instead, but there is a little bit of + // behaviour remaining that still depends on `ShellError::Return` + builder.push(Instruction::ReturnEarly { src: io_reg }.into_spanned(call.head))?; + + // io_reg is supposed to remain allocated + builder.load_empty(io_reg)?; + + Ok(()) +} diff --git a/nushell/crates/nu-engine/src/compile/mod.rs b/nushell/crates/nu-engine/src/compile/mod.rs new file mode 100644 index 0000000..5995e83 --- /dev/null +++ b/nushell/crates/nu-engine/src/compile/mod.rs @@ -0,0 +1,218 @@ +use nu_protocol::{ + CompileError, IntoSpanned, RegId, Span, + ast::{Block, Expr, Pipeline, PipelineRedirection, RedirectionSource, RedirectionTarget}, + engine::StateWorkingSet, + ir::{Instruction, IrBlock, RedirectMode}, +}; + +mod builder; +mod call; +mod expression; +mod keyword; +mod operator; +mod redirect; + +use builder::BlockBuilder; +use call::*; +use expression::compile_expression; +use operator::*; +use redirect::*; + +const BLOCK_INPUT: RegId = RegId::new(0); + +/// Compile Nushell pipeline abstract syntax tree (AST) to internal representation (IR) instructions +/// for evaluation. +pub fn compile(working_set: &StateWorkingSet, block: &Block) -> Result { + let mut builder = BlockBuilder::new(block.span); + + let span = block.span.unwrap_or(Span::unknown()); + + compile_block( + working_set, + &mut builder, + block, + RedirectModes::caller(span), + Some(BLOCK_INPUT), + BLOCK_INPUT, + )?; + + // A complete block has to end with a `return` + builder.push(Instruction::Return { src: BLOCK_INPUT }.into_spanned(span))?; + + builder.finish() +} + +/// Compiles a [`Block`] in-place into an IR block. This can be used in a nested manner, for example +/// by [`compile_if()`][keyword::compile_if], where the instructions for the blocks for the if/else +/// are inlined into the top-level IR block. +fn compile_block( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + block: &Block, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let span = block.span.unwrap_or(Span::unknown()); + let mut redirect_modes = Some(redirect_modes); + if !block.pipelines.is_empty() { + let last_index = block.pipelines.len() - 1; + for (index, pipeline) in block.pipelines.iter().enumerate() { + compile_pipeline( + working_set, + builder, + pipeline, + span, + // the redirect mode only applies to the last pipeline. + if index == last_index { + redirect_modes + .take() + .expect("should only take redirect_modes once") + } else { + RedirectModes::default() + }, + // input is only passed to the first pipeline. + if index == 0 { in_reg } else { None }, + out_reg, + )?; + + if index != last_index { + // Explicitly drain the out reg after each non-final pipeline, because that's how + // the semicolon functions. + if builder.is_allocated(out_reg) { + builder.push(Instruction::Drain { src: out_reg }.into_spanned(span))?; + } + builder.load_empty(out_reg)?; + } + } + Ok(()) + } else if in_reg.is_none() { + builder.load_empty(out_reg) + } else { + Ok(()) + } +} + +fn compile_pipeline( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + pipeline: &Pipeline, + fallback_span: Span, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let mut iter = pipeline.elements.iter().peekable(); + let mut in_reg = in_reg; + let mut redirect_modes = Some(redirect_modes); + while let Some(element) = iter.next() { + let span = element.pipe.unwrap_or(fallback_span); + + // We have to get the redirection mode from either the explicit redirection in the pipeline + // element, or from the next expression if it's specified there. If this is the last + // element, then it's from whatever is passed in as the mode to use. + + let next_redirect_modes = if let Some(next_element) = iter.peek() { + let mut modes = redirect_modes_of_expression(working_set, &next_element.expr, span)?; + + // If there's a next element with no inherent redirection we always pipe out *unless* + // this is a single redirection of stderr to pipe (e>|) + if modes.out.is_none() + && !matches!( + element.redirection, + Some(PipelineRedirection::Single { + source: RedirectionSource::Stderr, + target: RedirectionTarget::Pipe { .. } + }) + ) + { + let pipe_span = next_element.pipe.unwrap_or(next_element.expr.span); + modes.out = Some(RedirectMode::Pipe.into_spanned(pipe_span)); + } + + modes + } else { + redirect_modes + .take() + .expect("should only take redirect_modes once") + }; + + let spec_redirect_modes = match &element.redirection { + Some(PipelineRedirection::Single { source, target }) => { + let mode = redirection_target_to_mode(working_set, builder, target)?; + match source { + RedirectionSource::Stdout => RedirectModes { + out: Some(mode), + err: None, + }, + RedirectionSource::Stderr => RedirectModes { + out: None, + err: Some(mode), + }, + RedirectionSource::StdoutAndStderr => RedirectModes { + out: Some(mode), + err: Some(mode), + }, + } + } + Some(PipelineRedirection::Separate { out, err }) => { + // In this case, out and err must not both be Pipe + assert!( + !matches!( + (out, err), + ( + RedirectionTarget::Pipe { .. }, + RedirectionTarget::Pipe { .. } + ) + ), + "for Separate redirection, out and err targets must not both be Pipe" + ); + let out = redirection_target_to_mode(working_set, builder, out)?; + let err = redirection_target_to_mode(working_set, builder, err)?; + RedirectModes { + out: Some(out), + err: Some(err), + } + } + None => RedirectModes { + out: None, + err: None, + }, + }; + + let redirect_modes = RedirectModes { + out: spec_redirect_modes.out.or(next_redirect_modes.out), + err: spec_redirect_modes.err.or(next_redirect_modes.err), + }; + + compile_expression( + working_set, + builder, + &element.expr, + redirect_modes.clone(), + in_reg, + out_reg, + )?; + + // only clean up the redirection if current element is not + // a subexpression. The subexpression itself already clean it. + if !is_subexpression(&element.expr.expr) { + // Clean up the redirection + finish_redirection(builder, redirect_modes, out_reg)?; + } + + // The next pipeline element takes input from this output + in_reg = Some(out_reg); + } + Ok(()) +} + +fn is_subexpression(expr: &Expr) -> bool { + match expr { + Expr::FullCellPath(inner) => { + matches!(&inner.head.expr, &Expr::Subexpression(..)) + } + Expr::Subexpression(..) => true, + _ => false, + } +} diff --git a/nushell/crates/nu-engine/src/compile/operator.rs b/nushell/crates/nu-engine/src/compile/operator.rs new file mode 100644 index 0000000..5e67410 --- /dev/null +++ b/nushell/crates/nu-engine/src/compile/operator.rs @@ -0,0 +1,379 @@ +use nu_protocol::{ + ENV_VARIABLE_ID, IntoSpanned, RegId, Span, Spanned, Value, + ast::{Assignment, Boolean, CellPath, Expr, Expression, Math, Operator, PathMember, Pattern}, + engine::StateWorkingSet, + ir::{Instruction, Literal}, +}; +use nu_utils::IgnoreCaseExt; + +use super::{BlockBuilder, CompileError, RedirectModes, compile_expression}; + +pub(crate) fn compile_binary_op( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + lhs: &Expression, + op: Spanned, + rhs: &Expression, + span: Span, + out_reg: RegId, +) -> Result<(), CompileError> { + if let Operator::Assignment(assign_op) = op.item { + if let Some(decomposed_op) = decompose_assignment(assign_op) { + // Compiling an assignment that uses a binary op with the existing value + compile_binary_op( + working_set, + builder, + lhs, + decomposed_op.into_spanned(op.span), + rhs, + span, + out_reg, + )?; + } else { + // Compiling a plain assignment, where the current left-hand side value doesn't matter + compile_expression( + working_set, + builder, + rhs, + RedirectModes::value(rhs.span), + None, + out_reg, + )?; + } + + compile_assignment(working_set, builder, lhs, op.span, out_reg)?; + + // Load out_reg with Nothing, as that's the result of an assignment + builder.load_literal(out_reg, Literal::Nothing.into_spanned(op.span)) + } else { + // Not an assignment: just do the binary op + let lhs_reg = out_reg; + + compile_expression( + working_set, + builder, + lhs, + RedirectModes::value(lhs.span), + None, + lhs_reg, + )?; + + match op.item { + // `and` / `or` are short-circuiting, use `match` to avoid running the RHS if LHS is + // the correct value. Be careful to support and/or on non-boolean values + Operator::Boolean(bool_op @ Boolean::And) + | Operator::Boolean(bool_op @ Boolean::Or) => { + // `and` short-circuits on false, and `or` short-circuits on true. + let short_circuit_value = match bool_op { + Boolean::And => false, + Boolean::Or => true, + Boolean::Xor => unreachable!(), + }; + + // Before match against lhs_reg, it's important to collect it first to get a concrete value if there is a subexpression. + builder.push(Instruction::Collect { src_dst: lhs_reg }.into_spanned(lhs.span))?; + // Short-circuit to return `lhs_reg`. `match` op does not consume `lhs_reg`. + let short_circuit_label = builder.label(None); + builder.r#match( + Pattern::Value(Value::bool(short_circuit_value, op.span)), + lhs_reg, + short_circuit_label, + op.span, + )?; + + // If the match failed then this was not the short-circuit value, so we have to run + // the RHS expression + let rhs_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + rhs, + RedirectModes::value(rhs.span), + None, + rhs_reg, + )?; + + // It may seem intuitive that we can just return RHS here, but we do have to + // actually execute the binary-op in case this is not a boolean + builder.push( + Instruction::BinaryOp { + lhs_dst: lhs_reg, + op: Operator::Boolean(bool_op), + rhs: rhs_reg, + } + .into_spanned(op.span), + )?; + + // In either the short-circuit case or other case, the result is in lhs_reg = + // out_reg + builder.set_label(short_circuit_label, builder.here())?; + } + _ => { + // Any other operator, via `binary-op` + let rhs_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + rhs, + RedirectModes::value(rhs.span), + None, + rhs_reg, + )?; + + builder.push( + Instruction::BinaryOp { + lhs_dst: lhs_reg, + op: op.item, + rhs: rhs_reg, + } + .into_spanned(op.span), + )?; + } + } + + if lhs_reg != out_reg { + builder.push( + Instruction::Move { + dst: out_reg, + src: lhs_reg, + } + .into_spanned(op.span), + )?; + } + + builder.push(Instruction::Span { src_dst: out_reg }.into_spanned(span))?; + + Ok(()) + } +} + +/// The equivalent plain operator to use for an assignment, if any +pub(crate) fn decompose_assignment(assignment: Assignment) -> Option { + match assignment { + Assignment::Assign => None, + Assignment::AddAssign => Some(Operator::Math(Math::Add)), + Assignment::SubtractAssign => Some(Operator::Math(Math::Subtract)), + Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)), + Assignment::DivideAssign => Some(Operator::Math(Math::Divide)), + Assignment::ConcatenateAssign => Some(Operator::Math(Math::Concatenate)), + } +} + +/// Compile assignment of the value in a register to a left-hand expression +pub(crate) fn compile_assignment( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + lhs: &Expression, + assignment_span: Span, + rhs_reg: RegId, +) -> Result<(), CompileError> { + match lhs.expr { + Expr::Var(var_id) => { + // Double check that the variable is supposed to be mutable + if !working_set.get_variable(var_id).mutable { + return Err(CompileError::AssignmentRequiresMutableVar { span: lhs.span }); + } + + builder.push( + Instruction::StoreVariable { + var_id, + src: rhs_reg, + } + .into_spanned(assignment_span), + )?; + Ok(()) + } + Expr::FullCellPath(ref path) => match (&path.head, &path.tail) { + ( + Expression { + expr: Expr::Var(var_id), + .. + }, + _, + ) if *var_id == ENV_VARIABLE_ID => { + // This will be an assignment to an environment variable. + let Some(PathMember::String { val: key, .. }) = path.tail.first() else { + return Err(CompileError::CannotReplaceEnv { span: lhs.span }); + }; + + // Some env vars can't be set by Nushell code. + const AUTOMATIC_NAMES: &[&str] = &["PWD", "FILE_PWD", "CURRENT_FILE"]; + if AUTOMATIC_NAMES.iter().any(|name| key.eq_ignore_case(name)) { + return Err(CompileError::AutomaticEnvVarSetManually { + envvar_name: "PWD".into(), + span: lhs.span, + }); + } + + let key_data = builder.data(key)?; + + let val_reg = if path.tail.len() > 1 { + // Get the current value of the head and first tail of the path, from env + let head_reg = builder.next_register()?; + + // We could use compile_load_env, but this shares the key data... + // Always use optional, because it doesn't matter if it's already there + builder.push( + Instruction::LoadEnvOpt { + dst: head_reg, + key: key_data, + } + .into_spanned(lhs.span), + )?; + + // Default to empty record so we can do further upserts + let default_label = builder.label(None); + let upsert_label = builder.label(None); + builder.branch_if_empty(head_reg, default_label, assignment_span)?; + builder.jump(upsert_label, assignment_span)?; + + builder.set_label(default_label, builder.here())?; + builder.load_literal( + head_reg, + Literal::Record { capacity: 0 }.into_spanned(lhs.span), + )?; + + // Do the upsert on the current value to incorporate rhs + builder.set_label(upsert_label, builder.here())?; + compile_upsert_cell_path( + builder, + (&path.tail[1..]).into_spanned(lhs.span), + head_reg, + rhs_reg, + assignment_span, + )?; + + head_reg + } else { + // Path has only one tail, so we don't need the current value to do an upsert, + // just set it directly to rhs + rhs_reg + }; + + // Finally, store the modified env variable + builder.push( + Instruction::StoreEnv { + key: key_data, + src: val_reg, + } + .into_spanned(assignment_span), + )?; + Ok(()) + } + (_, tail) if tail.is_empty() => { + // If the path tail is empty, we can really just treat this as if it were an + // assignment to the head + compile_assignment(working_set, builder, &path.head, assignment_span, rhs_reg) + } + _ => { + // Just a normal assignment to some path + let head_reg = builder.next_register()?; + + // Compile getting current value of the head expression + compile_expression( + working_set, + builder, + &path.head, + RedirectModes::value(path.head.span), + None, + head_reg, + )?; + + // Upsert the tail of the path into the old value of the head expression + compile_upsert_cell_path( + builder, + path.tail.as_slice().into_spanned(lhs.span), + head_reg, + rhs_reg, + assignment_span, + )?; + + // Now compile the assignment of the updated value to the head + compile_assignment(working_set, builder, &path.head, assignment_span, head_reg) + } + }, + Expr::Garbage => Err(CompileError::Garbage { span: lhs.span }), + _ => Err(CompileError::AssignmentRequiresVar { span: lhs.span }), + } +} + +/// Compile an upsert-cell-path instruction, with known literal members +pub(crate) fn compile_upsert_cell_path( + builder: &mut BlockBuilder, + members: Spanned<&[PathMember]>, + src_dst: RegId, + new_value: RegId, + span: Span, +) -> Result<(), CompileError> { + let path_reg = builder.literal( + Literal::CellPath( + CellPath { + members: members.item.to_vec(), + } + .into(), + ) + .into_spanned(members.span), + )?; + builder.push( + Instruction::UpsertCellPath { + src_dst, + path: path_reg, + new_value, + } + .into_spanned(span), + )?; + Ok(()) +} + +/// Compile the correct sequence to get an environment variable + follow a path on it +pub(crate) fn compile_load_env( + builder: &mut BlockBuilder, + span: Span, + path: &[PathMember], + out_reg: RegId, +) -> Result<(), CompileError> { + match path { + [] => builder.push( + Instruction::LoadVariable { + dst: out_reg, + var_id: ENV_VARIABLE_ID, + } + .into_spanned(span), + )?, + [PathMember::Int { span, .. }, ..] => { + return Err(CompileError::AccessEnvByInt { span: *span }); + } + [ + PathMember::String { + val: key, optional, .. + }, + tail @ .., + ] => { + let key = builder.data(key)?; + + builder.push(if *optional { + Instruction::LoadEnvOpt { dst: out_reg, key }.into_spanned(span) + } else { + Instruction::LoadEnv { dst: out_reg, key }.into_spanned(span) + })?; + + if !tail.is_empty() { + let path = builder.literal( + Literal::CellPath(Box::new(CellPath { + members: tail.to_vec(), + })) + .into_spanned(span), + )?; + builder.push( + Instruction::FollowCellPath { + src_dst: out_reg, + path, + } + .into_spanned(span), + )?; + } + } + } + Ok(()) +} diff --git a/nushell/crates/nu-engine/src/compile/redirect.rs b/nushell/crates/nu-engine/src/compile/redirect.rs new file mode 100644 index 0000000..02011fb --- /dev/null +++ b/nushell/crates/nu-engine/src/compile/redirect.rs @@ -0,0 +1,159 @@ +use nu_protocol::{ + IntoSpanned, OutDest, RegId, Span, Spanned, + ast::{Expression, RedirectionTarget}, + engine::StateWorkingSet, + ir::{Instruction, RedirectMode}, +}; + +use super::{BlockBuilder, CompileError, compile_expression}; + +#[derive(Default, Clone)] +pub(crate) struct RedirectModes { + pub(crate) out: Option>, + pub(crate) err: Option>, +} + +impl RedirectModes { + pub(crate) fn value(span: Span) -> Self { + RedirectModes { + out: Some(RedirectMode::Value.into_spanned(span)), + err: None, + } + } + + pub(crate) fn caller(span: Span) -> RedirectModes { + RedirectModes { + out: Some(RedirectMode::Caller.into_spanned(span)), + err: Some(RedirectMode::Caller.into_spanned(span)), + } + } +} + +pub(crate) fn redirection_target_to_mode( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + target: &RedirectionTarget, +) -> Result, CompileError> { + Ok(match target { + RedirectionTarget::File { + expr, + append, + span: redir_span, + } => { + let file_num = builder.next_file_num()?; + let path_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::value(*redir_span), + None, + path_reg, + )?; + builder.push( + Instruction::OpenFile { + file_num, + path: path_reg, + append: *append, + } + .into_spanned(*redir_span), + )?; + RedirectMode::File { file_num }.into_spanned(*redir_span) + } + RedirectionTarget::Pipe { span } => RedirectMode::Pipe.into_spanned(*span), + }) +} + +pub(crate) fn redirect_modes_of_expression( + working_set: &StateWorkingSet, + expression: &Expression, + redir_span: Span, +) -> Result { + let (out, err) = expression.expr.pipe_redirection(working_set); + Ok(RedirectModes { + out: out + .map(|r| r.into_spanned(redir_span)) + .map(out_dest_to_redirect_mode) + .transpose()?, + err: err + .map(|r| r.into_spanned(redir_span)) + .map(out_dest_to_redirect_mode) + .transpose()?, + }) +} + +/// Finish the redirection for an expression, writing to and closing files as necessary +pub(crate) fn finish_redirection( + builder: &mut BlockBuilder, + modes: RedirectModes, + out_reg: RegId, +) -> Result<(), CompileError> { + if let Some(Spanned { + item: RedirectMode::File { file_num }, + span, + }) = modes.out + { + // If out is a file and err is a pipe, we must not consume the expression result - + // that is actually the err, in that case. + if !matches!( + modes.err, + Some(Spanned { + item: RedirectMode::Pipe, + .. + }) + ) { + builder.push( + Instruction::WriteFile { + file_num, + src: out_reg, + } + .into_spanned(span), + )?; + builder.load_empty(out_reg)?; + } + builder.push(Instruction::CloseFile { file_num }.into_spanned(span))?; + } + + match modes.err { + Some(Spanned { + item: RedirectMode::File { file_num }, + span, + }) => { + // Close the file, unless it's the same as out (in which case it was already closed) + if !modes.out.is_some_and(|out_mode| match out_mode.item { + RedirectMode::File { + file_num: out_file_num, + } => file_num == out_file_num, + _ => false, + }) { + builder.push(Instruction::CloseFile { file_num }.into_spanned(span))?; + } + } + Some(Spanned { + item: RedirectMode::Pipe, + span, + }) => { + builder.push(Instruction::CheckErrRedirected { src: out_reg }.into_spanned(span))?; + } + _ => (), + } + + Ok(()) +} + +pub(crate) fn out_dest_to_redirect_mode( + out_dest: Spanned, +) -> Result, CompileError> { + let span = out_dest.span; + out_dest + .map(|out_dest| match out_dest { + OutDest::Pipe => Ok(RedirectMode::Pipe), + OutDest::PipeSeparate => Ok(RedirectMode::PipeSeparate), + OutDest::Value => Ok(RedirectMode::Value), + OutDest::Null => Ok(RedirectMode::Null), + OutDest::Print => Ok(RedirectMode::Print), + OutDest::Inherit => Err(CompileError::InvalidRedirectMode { span }), + OutDest::File(_) => Err(CompileError::InvalidRedirectMode { span }), + }) + .transpose() +} diff --git a/nushell/crates/nu-engine/src/documentation.rs b/nushell/crates/nu-engine/src/documentation.rs new file mode 100644 index 0000000..36f4a30 --- /dev/null +++ b/nushell/crates/nu-engine/src/documentation.rs @@ -0,0 +1,599 @@ +use crate::eval_call; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, PositionalArg, Signature, Span, + SpanId, Spanned, SyntaxShape, Type, Value, + ast::{Argument, Call, Expr, Expression, RecordItem}, + debugger::WithoutDebug, + engine::CommandType, + engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID}, + record, +}; +use nu_utils::terminal_size; +use std::{collections::HashMap, fmt::Write}; + +/// ANSI style reset +const RESET: &str = "\x1b[0m"; +/// ANSI set default color (as set in the terminal) +const DEFAULT_COLOR: &str = "\x1b[39m"; + +pub fn get_full_help( + command: &dyn Command, + engine_state: &EngineState, + stack: &mut Stack, +) -> String { + // Precautionary step to capture any command output generated during this operation. We + // internally call several commands (`table`, `ansi`, `nu-highlight`) and get their + // `PipelineData` using this `Stack`, any other output should not be redirected like the main + // execution. + let stack = &mut stack.start_collect_value(); + + let signature = engine_state + .get_signature(command) + .update_from_command(command); + + get_documentation( + &signature, + &command.examples(), + engine_state, + stack, + command.is_keyword(), + ) +} + +/// Syntax highlight code using the `nu-highlight` command if available +fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String { + if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) { + let decl = engine_state.get_decl(highlighter); + + let call = Call::new(Span::unknown()); + + if let Ok(output) = decl.run( + engine_state, + stack, + &(&call).into(), + Value::string(code_string, Span::unknown()).into_pipeline_data(), + ) { + let result = output.into_value(Span::unknown()); + if let Ok(s) = result.and_then(Value::coerce_into_string) { + return s; // successfully highlighted string + } + } + } + code_string.to_string() +} + +fn get_documentation( + sig: &Signature, + examples: &[Example], + engine_state: &EngineState, + stack: &mut Stack, + is_parser_keyword: bool, +) -> String { + let nu_config = stack.get_config(engine_state); + + // Create ansi colors + let mut help_style = HelpStyle::default(); + help_style.update_from_config(engine_state, &nu_config); + let help_section_name = &help_style.section_name; + let help_subcolor_one = &help_style.subcolor_one; + + let cmd_name = &sig.name; + let mut long_desc = String::new(); + + let desc = &sig.description; + if !desc.is_empty() { + long_desc.push_str(desc); + long_desc.push_str("\n\n"); + } + + let extra_desc = &sig.extra_description; + if !extra_desc.is_empty() { + long_desc.push_str(extra_desc); + long_desc.push_str("\n\n"); + } + + if !sig.search_terms.is_empty() { + let _ = write!( + long_desc, + "{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{RESET}\n\n", + sig.search_terms.join(", "), + ); + } + + let _ = write!( + long_desc, + "{help_section_name}Usage{RESET}:\n > {}\n", + sig.call_signature() + ); + + // TODO: improve the subcommand name resolution + // issues: + // - Aliases are included + // - https://github.com/nushell/nushell/issues/11657 + // - Subcommands are included violating module scoping + // - https://github.com/nushell/nushell/issues/11447 + // - https://github.com/nushell/nushell/issues/11625 + let mut subcommands = vec![]; + let signatures = engine_state.get_signatures_and_declids(true); + for (sig, decl_id) in signatures { + let command_type = engine_state.get_decl(decl_id).command_type(); + + // Don't display removed/deprecated commands in the Subcommands list + if sig.name.starts_with(&format!("{cmd_name} ")) + && !matches!(sig.category, Category::Removed) + { + // If it's a plugin, alias, or custom command, display that information in the help + if command_type == CommandType::Plugin + || command_type == CommandType::Alias + || command_type == CommandType::Custom + { + subcommands.push(format!( + " {help_subcolor_one}{} {help_section_name}({}){RESET} - {}", + sig.name, command_type, sig.description + )); + } else { + subcommands.push(format!( + " {help_subcolor_one}{}{RESET} - {}", + sig.name, sig.description + )); + } + } + } + + if !subcommands.is_empty() { + let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n"); + subcommands.sort(); + long_desc.push_str(&subcommands.join("\n")); + long_desc.push('\n'); + } + + if !sig.named.is_empty() { + long_desc.push_str(&get_flags_section(sig, &help_style, |v| { + nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack) + })) + } + + if !sig.required_positional.is_empty() + || !sig.optional_positional.is_empty() + || sig.rest_positional.is_some() + { + let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n"); + for positional in &sig.required_positional { + write_positional( + &mut long_desc, + positional, + PositionalKind::Required, + &help_style, + &nu_config, + engine_state, + stack, + ); + } + for positional in &sig.optional_positional { + write_positional( + &mut long_desc, + positional, + PositionalKind::Optional, + &help_style, + &nu_config, + engine_state, + stack, + ); + } + + if let Some(rest_positional) = &sig.rest_positional { + write_positional( + &mut long_desc, + rest_positional, + PositionalKind::Rest, + &help_style, + &nu_config, + engine_state, + stack, + ); + } + } + + fn get_term_width() -> usize { + if let Ok((w, _h)) = terminal_size() { + w as usize + } else { + 80 + } + } + + if !is_parser_keyword && !sig.input_output_types.is_empty() { + if let Some(decl_id) = engine_state.find_decl(b"table", &[]) { + // FIXME: we may want to make this the span of the help command in the future + let span = Span::unknown(); + let mut vals = vec![]; + for (input, output) in &sig.input_output_types { + vals.push(Value::record( + record! { + "input" => Value::string(input.to_string(), span), + "output" => Value::string(output.to_string(), span), + }, + span, + )); + } + + let caller_stack = &mut Stack::new().collect_value(); + if let Ok(result) = eval_call::( + engine_state, + caller_stack, + &Call { + decl_id, + head: span, + arguments: vec![Argument::Named(( + Spanned { + item: "width".to_string(), + span: Span::unknown(), + }, + None, + Some(Expression::new_unknown( + Expr::Int(get_term_width() as i64 - 2), // padding, see below + Span::unknown(), + Type::Int, + )), + ))], + parser_info: HashMap::new(), + }, + PipelineData::Value(Value::list(vals, span), None), + ) { + if let Ok((str, ..)) = result.collect_string_strict(span) { + let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:"); + for line in str.lines() { + let _ = writeln!(long_desc, " {line}"); + } + } + } + } + } + + if !examples.is_empty() { + let _ = write!(long_desc, "\n{help_section_name}Examples{RESET}:"); + } + + for example in examples { + long_desc.push('\n'); + long_desc.push_str(" "); + long_desc.push_str(example.description); + + if !nu_config.use_ansi_coloring.get(engine_state) { + let _ = write!(long_desc, "\n > {}\n", example.example); + } else { + let code_string = nu_highlight_string(example.example, engine_state, stack); + let _ = write!(long_desc, "\n > {code_string}\n"); + }; + + if let Some(result) = &example.result { + let mut table_call = Call::new(Span::unknown()); + if example.example.ends_with("--collapse") { + // collapse the result + table_call.add_named(( + Spanned { + item: "collapse".to_string(), + span: Span::unknown(), + }, + None, + None, + )) + } else { + // expand the result + table_call.add_named(( + Spanned { + item: "expand".to_string(), + span: Span::unknown(), + }, + None, + None, + )) + } + table_call.add_named(( + Spanned { + item: "width".to_string(), + span: Span::unknown(), + }, + None, + Some(Expression::new_unknown( + Expr::Int(get_term_width() as i64 - 2), + Span::unknown(), + Type::Int, + )), + )); + + let table = engine_state + .find_decl("table".as_bytes(), &[]) + .and_then(|decl_id| { + engine_state + .get_decl(decl_id) + .run( + engine_state, + stack, + &(&table_call).into(), + PipelineData::Value(result.clone(), None), + ) + .ok() + }); + + for item in table.into_iter().flatten() { + let _ = writeln!( + long_desc, + " {}", + item.to_expanded_string("", &nu_config) + .replace('\n', "\n ") + .trim() + ); + } + } + } + + long_desc.push('\n'); + + if !nu_config.use_ansi_coloring.get(engine_state) { + nu_utils::strip_ansi_string_likely(long_desc) + } else { + long_desc + } +} + +fn update_ansi_from_config( + ansi_code: &mut String, + engine_state: &EngineState, + nu_config: &Config, + theme_component: &str, +) { + if let Some(color) = &nu_config.color_config.get(theme_component) { + let caller_stack = &mut Stack::new().collect_value(); + let span = Span::unknown(); + let span_id = UNKNOWN_SPAN_ID; + + let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id); + + // Call ansi command using argument + if let Some(argument) = argument_opt { + if let Some(decl_id) = engine_state.find_decl(b"ansi", &[]) { + if let Ok(result) = eval_call::( + engine_state, + caller_stack, + &Call { + decl_id, + head: span, + arguments: vec![argument], + parser_info: HashMap::new(), + }, + PipelineData::Empty, + ) { + if let Ok((str, ..)) = result.collect_string_strict(span) { + *ansi_code = str; + } + } + } + } + } +} + +fn get_argument_for_color_value( + nu_config: &Config, + color: &Value, + span: Span, + span_id: SpanId, +) -> Option { + match color { + Value::Record { val, .. } => { + let record_exp: Vec = (**val) + .iter() + .map(|(k, v)| { + RecordItem::Pair( + Expression::new_existing( + Expr::String(k.clone()), + span, + span_id, + Type::String, + ), + Expression::new_existing( + Expr::String(v.clone().to_expanded_string("", nu_config)), + span, + span_id, + Type::String, + ), + ) + }) + .collect(); + + Some(Argument::Positional(Expression::new_existing( + Expr::Record(record_exp), + Span::unknown(), + UNKNOWN_SPAN_ID, + Type::Record( + [ + ("fg".to_string(), Type::String), + ("attr".to_string(), Type::String), + ] + .into(), + ), + ))) + } + Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing( + Expr::String(val.clone()), + Span::unknown(), + UNKNOWN_SPAN_ID, + Type::String, + ))), + _ => None, + } +} + +/// Contains the settings for ANSI colors in help output +/// +/// By default contains a fixed set of (4-bit) colors +/// +/// Can reflect configuration using [`HelpStyle::update_from_config`] +pub struct HelpStyle { + section_name: String, + subcolor_one: String, + subcolor_two: String, +} + +impl Default for HelpStyle { + fn default() -> Self { + HelpStyle { + // default: green + section_name: "\x1b[32m".to_string(), + // default: cyan + subcolor_one: "\x1b[36m".to_string(), + // default: light blue + subcolor_two: "\x1b[94m".to_string(), + } + } +} + +impl HelpStyle { + /// Pull colors from the [`Config`] + /// + /// Uses some arbitrary `shape_*` settings, assuming they are well visible in the terminal theme. + /// + /// Implementation detail: currently executes `ansi` command internally thus requiring the + /// [`EngineState`] for execution. + /// See for details + pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) { + update_ansi_from_config( + &mut self.section_name, + engine_state, + nu_config, + "shape_string", + ); + update_ansi_from_config( + &mut self.subcolor_one, + engine_state, + nu_config, + "shape_external", + ); + update_ansi_from_config( + &mut self.subcolor_two, + engine_state, + nu_config, + "shape_block", + ); + } +} + +/// Make syntax shape presentable by stripping custom completer info +fn document_shape(shape: &SyntaxShape) -> &SyntaxShape { + match shape { + SyntaxShape::CompleterWrapper(inner_shape, _) => inner_shape, + _ => shape, + } +} + +#[derive(PartialEq)] +enum PositionalKind { + Required, + Optional, + Rest, +} + +fn write_positional( + long_desc: &mut String, + positional: &PositionalArg, + arg_kind: PositionalKind, + help_style: &HelpStyle, + nu_config: &Config, + engine_state: &EngineState, + stack: &mut Stack, +) { + let help_subcolor_one = &help_style.subcolor_one; + let help_subcolor_two = &help_style.subcolor_two; + + // Indentation + long_desc.push_str(" "); + if arg_kind == PositionalKind::Rest { + long_desc.push_str("..."); + } + match &positional.shape { + SyntaxShape::Keyword(kw, shape) => { + let _ = write!( + long_desc, + "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>", + String::from_utf8_lossy(kw), + document_shape(shape), + ); + } + _ => { + let _ = write!( + long_desc, + "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>", + positional.name, + document_shape(&positional.shape), + ); + } + }; + if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional { + let _ = write!(long_desc, ": {}", positional.desc); + } + if arg_kind == PositionalKind::Optional { + if let Some(value) = &positional.default_value { + let _ = write!( + long_desc, + " (optional, default: {})", + nu_highlight_string( + &value.to_parsable_string(", ", nu_config), + engine_state, + stack + ) + ); + } else { + long_desc.push_str(" (optional)"); + }; + } + long_desc.push('\n'); +} + +pub fn get_flags_section( + signature: &Signature, + help_style: &HelpStyle, + mut value_formatter: F, // format default Value (because some calls cant access config or nu-highlight) +) -> String +where + F: FnMut(&nu_protocol::Value) -> String, +{ + let help_section_name = &help_style.section_name; + let help_subcolor_one = &help_style.subcolor_one; + let help_subcolor_two = &help_style.subcolor_two; + + let mut long_desc = String::new(); + let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n"); + for flag in &signature.named { + // Indentation + long_desc.push_str(" "); + // Short flag shown before long flag + if let Some(short) = flag.short { + let _ = write!(long_desc, "{help_subcolor_one}-{}{RESET}", short); + if !flag.long.is_empty() { + let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} "); + } + } + if !flag.long.is_empty() { + let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long); + } + if flag.required { + long_desc.push_str(" (required parameter)") + } + // Type/Syntax shape info + if let Some(arg) = &flag.arg { + let _ = write!( + long_desc, + " <{help_subcolor_two}{}{RESET}>", + document_shape(arg) + ); + } + if !flag.desc.is_empty() { + let _ = write!(long_desc, ": {}", flag.desc); + } + if let Some(value) = &flag.default_value { + let _ = write!(long_desc, " (default: {})", &value_formatter(value)); + } + long_desc.push('\n'); + } + long_desc +} diff --git a/nushell/crates/nu-engine/src/env.rs b/nushell/crates/nu-engine/src/env.rs new file mode 100644 index 0000000..e02d762 --- /dev/null +++ b/nushell/crates/nu-engine/src/env.rs @@ -0,0 +1,431 @@ +use crate::ClosureEvalOnce; +use nu_path::canonicalize_with; +use nu_protocol::{ + ShellError, Span, Type, Value, VarId, + ast::Expr, + engine::{Call, EngineState, Stack, StateWorkingSet}, + shell_error::io::{IoError, IoErrorExt, NotFound}, +}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, +}; + +pub const ENV_CONVERSIONS: &str = "ENV_CONVERSIONS"; + +enum ConversionError { + ShellError(ShellError), + CellPathError, +} + +impl From for ConversionError { + fn from(value: ShellError) -> Self { + Self::ShellError(value) + } +} + +/// Translate environment variables from Strings to Values. +pub fn convert_env_vars( + stack: &mut Stack, + engine_state: &EngineState, + conversions: &Value, +) -> Result<(), ShellError> { + let conversions = conversions.as_record()?; + for (key, conversion) in conversions.into_iter() { + if let Some((case_preserve_env_name, val)) = + stack.get_env_var_insensitive(engine_state, key) + { + match val.get_type() { + Type::String => {} + _ => continue, + } + + let conversion = conversion + .as_record()? + .get("from_string") + .ok_or(ShellError::MissingRequiredColumn { + column: "from_string", + span: conversion.span(), + })? + .as_closure()?; + + let new_val = ClosureEvalOnce::new(engine_state, stack, conversion.clone()) + .debug(false) + .run_with_value(val.clone())? + .into_value(val.span())?; + + stack.add_env_var(case_preserve_env_name.to_string(), new_val); + } + } + Ok(()) +} + +/// Translate environment variables from Strings to Values. Requires config to be already set up in +/// case the user defined custom env conversions in config.nu. +/// +/// It returns Option instead of Result since we do want to translate all the values we can and +/// skip errors. This function is called in the main() so we want to keep running, we cannot just +/// exit. +pub fn convert_env_values( + engine_state: &mut EngineState, + stack: &mut Stack, +) -> Result<(), ShellError> { + let mut error = None; + + let mut new_scope = HashMap::new(); + + let env_vars = engine_state.render_env_vars(); + + for (name, val) in env_vars { + if let Value::String { .. } = val { + // Only run from_string on string values + match get_converted_value(engine_state, stack, name, val, "from_string") { + Ok(v) => { + let _ = new_scope.insert(name.to_string(), v); + } + Err(ConversionError::ShellError(e)) => error = error.or(Some(e)), + Err(ConversionError::CellPathError) => { + let _ = new_scope.insert(name.to_string(), val.clone()); + } + } + } else { + // Skip values that are already converted (not a string) + let _ = new_scope.insert(name.to_string(), val.clone()); + } + } + + error = error.or_else(|| ensure_path(engine_state, stack)); + + if let Ok(last_overlay_name) = &stack.last_overlay_name() { + if let Some(env_vars) = Arc::make_mut(&mut engine_state.env_vars).get_mut(last_overlay_name) + { + for (k, v) in new_scope { + env_vars.insert(k, v); + } + } else { + error = error.or_else(|| { + Some(ShellError::NushellFailedHelp { msg: "Last active overlay not found in permanent state.".into(), help: "This error happened during the conversion of environment variables from strings to Nushell values.".into() }) + }); + } + } else { + error = error.or_else(|| { + Some(ShellError::NushellFailedHelp { msg: "Last active overlay not found in stack.".into(), help: "This error happened during the conversion of environment variables from strings to Nushell values.".into() }) + }); + } + + if let Some(err) = error { + Err(err) + } else { + Ok(()) + } +} + +/// Translate one environment variable from Value to String +/// +/// Returns Ok(None) if the env var is not +pub fn env_to_string( + env_name: &str, + value: &Value, + engine_state: &EngineState, + stack: &Stack, +) -> Result { + match get_converted_value(engine_state, stack, env_name, value, "to_string") { + Ok(v) => Ok(v.coerce_into_string()?), + Err(ConversionError::ShellError(e)) => Err(e), + Err(ConversionError::CellPathError) => match value.coerce_string() { + Ok(s) => Ok(s), + Err(_) => { + if env_name.to_lowercase() == "path" { + // Try to convert PATH/Path list to a string + match value { + Value::List { vals, .. } => { + let paths: Vec = vals + .iter() + .filter_map(|v| v.coerce_str().ok()) + .map(|s| nu_path::expand_tilde(&*s).to_string_lossy().into_owned()) + .collect(); + + std::env::join_paths(paths.iter().map(AsRef::::as_ref)) + .map(|p| p.to_string_lossy().to_string()) + .map_err(|_| ShellError::EnvVarNotAString { + envvar_name: env_name.to_string(), + span: value.span(), + }) + } + _ => Err(ShellError::EnvVarNotAString { + envvar_name: env_name.to_string(), + span: value.span(), + }), + } + } else { + Err(ShellError::EnvVarNotAString { + envvar_name: env_name.to_string(), + span: value.span(), + }) + } + } + }, + } +} + +/// Translate all environment variables from Values to Strings +pub fn env_to_strings( + engine_state: &EngineState, + stack: &Stack, +) -> Result, ShellError> { + let env_vars = stack.get_env_vars(engine_state); + let mut env_vars_str = HashMap::new(); + for (env_name, val) in env_vars { + match env_to_string(&env_name, &val, engine_state, stack) { + Ok(val_str) => { + env_vars_str.insert(env_name, val_str); + } + Err(ShellError::EnvVarNotAString { .. }) => {} // ignore non-string values + Err(e) => return Err(e), + } + } + + Ok(env_vars_str) +} + +/// Returns the current working directory as a String, which is guaranteed to be canonicalized. +/// Unlike `current_dir_str_const()`, this also considers modifications to the current working directory made on the stack. +/// +/// Returns an error if $env.PWD doesn't exist, is not a String, or is not an absolute path. +#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")] +pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result { + #[allow(deprecated)] + current_dir(engine_state, stack).map(|path| path.to_string_lossy().to_string()) +} + +/// Returns the current working directory as a String, which is guaranteed to be canonicalized. +/// +/// Returns an error if $env.PWD doesn't exist, is not a String, or is not an absolute path. +#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")] +pub fn current_dir_str_const(working_set: &StateWorkingSet) -> Result { + #[allow(deprecated)] + current_dir_const(working_set).map(|path| path.to_string_lossy().to_string()) +} + +/// Returns the current working directory, which is guaranteed to be canonicalized. +/// Unlike `current_dir_const()`, this also considers modifications to the current working directory made on the stack. +/// +/// Returns an error if $env.PWD doesn't exist, is not a String, or is not an absolute path. +#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")] +pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result { + let cwd = engine_state.cwd(Some(stack))?; + // `EngineState::cwd()` always returns absolute path. + // We're using `canonicalize_with` instead of `fs::canonicalize()` because + // we still need to simplify Windows paths. "." is safe because `cwd` should + // be an absolute path already. + canonicalize_with(&cwd, ".").map_err(|err| { + ShellError::Io(IoError::new_internal_with_path( + err.not_found_as(NotFound::Directory), + "Could not canonicalize current dir", + nu_protocol::location!(), + PathBuf::from(cwd), + )) + }) +} + +/// Returns the current working directory, which is guaranteed to be canonicalized. +/// +/// Returns an error if $env.PWD doesn't exist, is not a String, or is not an absolute path. +#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")] +pub fn current_dir_const(working_set: &StateWorkingSet) -> Result { + let cwd = working_set.permanent_state.cwd(None)?; + // `EngineState::cwd()` always returns absolute path. + // We're using `canonicalize_with` instead of `fs::canonicalize()` because + // we still need to simplify Windows paths. "." is safe because `cwd` should + // be an absolute path already. + canonicalize_with(&cwd, ".").map_err(|err| { + ShellError::Io(IoError::new_internal_with_path( + err.not_found_as(NotFound::Directory), + "Could not canonicalize current dir", + nu_protocol::location!(), + PathBuf::from(cwd), + )) + }) +} + +/// Get the contents of path environment variable as a list of strings +pub fn path_str( + engine_state: &EngineState, + stack: &Stack, + span: Span, +) -> Result { + let (pathname, pathval) = match stack.get_env_var_insensitive(engine_state, "path") { + Some((_, v)) => Ok((if cfg!(windows) { "Path" } else { "PATH" }, v)), + None => Err(ShellError::EnvVarNotFoundAtRuntime { + envvar_name: if cfg!(windows) { + "Path".to_string() + } else { + "PATH".to_string() + }, + span, + }), + }?; + + env_to_string(pathname, pathval, engine_state, stack) +} + +pub const DIR_VAR_PARSER_INFO: &str = "dirs_var"; +pub fn get_dirs_var_from_call(stack: &Stack, call: &Call) -> Option { + call.get_parser_info(stack, DIR_VAR_PARSER_INFO) + .and_then(|x| { + if let Expr::Var(id) = x.expr { + Some(id) + } else { + None + } + }) +} + +/// This helper function is used to find files during eval +/// +/// First, the actual current working directory is selected as +/// a) the directory of a file currently being parsed +/// b) current working directory (PWD) +/// +/// Then, if the file is not found in the actual cwd, NU_LIB_DIRS is checked. +/// If there is a relative path in NU_LIB_DIRS, it is assumed to be relative to the actual cwd +/// determined in the first step. +/// +/// Always returns an absolute path +pub fn find_in_dirs_env( + filename: &str, + engine_state: &EngineState, + stack: &Stack, + dirs_var: Option, +) -> Result, ShellError> { + // Choose whether to use file-relative or PWD-relative path + let cwd = if let Some(pwd) = stack.get_env_var(engine_state, "FILE_PWD") { + match env_to_string("FILE_PWD", pwd, engine_state, stack) { + Ok(cwd) => { + if Path::new(&cwd).is_absolute() { + cwd + } else { + return Err(ShellError::GenericError { + error: "Invalid current directory".into(), + msg: format!( + "The 'FILE_PWD' environment variable must be set to an absolute path. Found: '{cwd}'" + ), + span: Some(pwd.span()), + help: None, + inner: vec![], + }); + } + } + Err(e) => return Err(e), + } + } else { + engine_state.cwd_as_string(Some(stack))? + }; + + let check_dir = |lib_dirs: Option<&Value>| -> Option { + if let Ok(p) = canonicalize_with(filename, &cwd) { + return Some(p); + } + let path = Path::new(filename); + if !path.is_relative() { + return None; + } + + lib_dirs? + .as_list() + .ok()? + .iter() + .map(|lib_dir| -> Option { + let dir = lib_dir.to_path().ok()?; + let dir_abs = canonicalize_with(dir, &cwd).ok()?; + canonicalize_with(filename, dir_abs).ok() + }) + .find(Option::is_some) + .flatten() + }; + + let lib_dirs = dirs_var.and_then(|var_id| engine_state.get_var(var_id).const_val.as_ref()); + // TODO: remove (see #8310) + let lib_dirs_fallback = stack.get_env_var(engine_state, "NU_LIB_DIRS"); + + Ok(check_dir(lib_dirs).or_else(|| check_dir(lib_dirs_fallback))) +} + +fn get_converted_value( + engine_state: &EngineState, + stack: &Stack, + name: &str, + orig_val: &Value, + direction: &str, +) -> Result { + let conversion = stack + .get_env_var(engine_state, ENV_CONVERSIONS) + .ok_or(ConversionError::CellPathError)? + .as_record()? + .get(name) + .ok_or(ConversionError::CellPathError)? + .as_record()? + .get(direction) + .ok_or(ConversionError::CellPathError)? + .as_closure()?; + + Ok( + ClosureEvalOnce::new(engine_state, stack, conversion.clone()) + .debug(false) + .run_with_value(orig_val.clone())? + .into_value(orig_val.span())?, + ) +} + +fn ensure_path(engine_state: &EngineState, stack: &mut Stack) -> Option { + let mut error = None; + + // If PATH/Path is still a string, force-convert it to a list + if let Some((preserve_case_name, value)) = stack.get_env_var_insensitive(engine_state, "Path") { + let span = value.span(); + match value { + Value::String { val, .. } => { + // Force-split path into a list + let paths = std::env::split_paths(val) + .map(|p| Value::string(p.to_string_lossy().to_string(), span)) + .collect(); + + stack.add_env_var(preserve_case_name.to_string(), Value::list(paths, span)); + } + Value::List { vals, .. } => { + // Must be a list of strings + if !vals.iter().all(|v| matches!(v, Value::String { .. })) { + error = error.or_else(|| { + Some(ShellError::GenericError { + error: format!( + "Incorrect {preserve_case_name} environment variable value" + ), + msg: format!("{preserve_case_name} must be a list of strings"), + span: Some(span), + help: None, + inner: vec![], + }) + }); + } + } + + val => { + // All other values are errors + let span = val.span(); + + error = error.or_else(|| { + Some(ShellError::GenericError { + error: format!("Incorrect {preserve_case_name} environment variable value"), + msg: format!("{preserve_case_name} must be a list of strings"), + span: Some(span), + help: None, + inner: vec![], + }) + }); + } + } + } + + error +} diff --git a/nushell/crates/nu-engine/src/eval.rs b/nushell/crates/nu-engine/src/eval.rs new file mode 100644 index 0000000..6fecf9d --- /dev/null +++ b/nushell/crates/nu-engine/src/eval.rs @@ -0,0 +1,651 @@ +use crate::eval_ir_block; +#[allow(deprecated)] +use crate::get_full_help; +use nu_protocol::{ + BlockId, Config, DataSource, ENV_VARIABLE_ID, IntoPipelineData, PipelineData, PipelineMetadata, + ShellError, Span, Value, VarId, + ast::{Assignment, Block, Call, Expr, Expression, ExternalArgument, PathMember}, + debugger::DebugContext, + engine::{Closure, EngineState, Stack}, + eval_base::Eval, +}; +use nu_utils::IgnoreCaseExt; +use std::sync::Arc; + +pub fn eval_call( + engine_state: &EngineState, + caller_stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + engine_state.signals().check(call.head)?; + let decl = engine_state.get_decl(call.decl_id); + + if !decl.is_known_external() && call.named_iter().any(|(flag, _, _)| flag.item == "help") { + let help = get_full_help(decl, engine_state, caller_stack); + Ok(Value::string(help, call.head).into_pipeline_data()) + } else if let Some(block_id) = decl.block_id() { + let block = engine_state.get_block(block_id); + + let mut callee_stack = caller_stack.gather_captures(engine_state, &block.captures); + + // Rust does not check recursion limits outside of const evaluation. + // But nu programs run in the same process as the shell. + // To prevent a stack overflow in user code from crashing the shell, + // we limit the recursion depth of function calls. + // Picked 50 arbitrarily, should work on all architectures. + let maximum_call_stack_depth: u64 = engine_state.config.recursion_limit as u64; + callee_stack.recursion_count += 1; + if callee_stack.recursion_count > maximum_call_stack_depth { + callee_stack.recursion_count = 0; + return Err(ShellError::RecursionLimitReached { + recursion_limit: maximum_call_stack_depth, + span: block.span, + }); + } + + for (param_idx, (param, required)) in decl + .signature() + .required_positional + .iter() + .map(|p| (p, true)) + .chain( + decl.signature() + .optional_positional + .iter() + .map(|p| (p, false)), + ) + .enumerate() + { + let var_id = param + .var_id + .expect("internal error: all custom parameters must have var_ids"); + + if let Some(arg) = call.positional_nth(param_idx) { + let result = eval_expression::(engine_state, caller_stack, arg)?; + let param_type = param.shape.to_type(); + if required && !result.is_subtype_of(¶m_type) { + return Err(ShellError::CantConvert { + to_type: param.shape.to_type().to_string(), + from_type: result.get_type().to_string(), + span: result.span(), + help: None, + }); + } + callee_stack.add_var(var_id, result); + } else if let Some(value) = ¶m.default_value { + callee_stack.add_var(var_id, value.to_owned()); + } else { + callee_stack.add_var(var_id, Value::nothing(call.head)); + } + } + + if let Some(rest_positional) = decl.signature().rest_positional { + let mut rest_items = vec![]; + + for result in call.rest_iter_flattened( + decl.signature().required_positional.len() + + decl.signature().optional_positional.len(), + |expr| eval_expression::(engine_state, caller_stack, expr), + )? { + rest_items.push(result); + } + + let span = if let Some(rest_item) = rest_items.first() { + rest_item.span() + } else { + call.head + }; + + callee_stack.add_var( + rest_positional + .var_id + .expect("Internal error: rest positional parameter lacks var_id"), + Value::list(rest_items, span), + ) + } + + for named in decl.signature().named { + if let Some(var_id) = named.var_id { + let mut found = false; + for call_named in call.named_iter() { + if let (Some(spanned), Some(short)) = (&call_named.1, named.short) { + if spanned.item == short.to_string() { + if let Some(arg) = &call_named.2 { + let result = eval_expression::(engine_state, caller_stack, arg)?; + + callee_stack.add_var(var_id, result); + } else if let Some(value) = &named.default_value { + callee_stack.add_var(var_id, value.to_owned()); + } else { + callee_stack.add_var(var_id, Value::bool(true, call.head)) + } + found = true; + } + } else if call_named.0.item == named.long { + if let Some(arg) = &call_named.2 { + let result = eval_expression::(engine_state, caller_stack, arg)?; + + callee_stack.add_var(var_id, result); + } else if let Some(value) = &named.default_value { + callee_stack.add_var(var_id, value.to_owned()); + } else { + callee_stack.add_var(var_id, Value::bool(true, call.head)) + } + found = true; + } + } + + if !found { + if named.arg.is_none() { + callee_stack.add_var(var_id, Value::bool(false, call.head)) + } else if let Some(value) = named.default_value { + callee_stack.add_var(var_id, value); + } else { + callee_stack.add_var(var_id, Value::nothing(call.head)) + } + } + } + } + + let result = + eval_block_with_early_return::(engine_state, &mut callee_stack, block, input); + + if block.redirect_env { + redirect_env(engine_state, caller_stack, &callee_stack); + } + + result + } else { + // We pass caller_stack here with the knowledge that internal commands + // are going to be specifically looking for global state in the stack + // rather than any local state. + decl.run(engine_state, caller_stack, &call.into(), input) + } +} + +/// Redirect the environment from callee to the caller. +pub fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee_stack: &Stack) { + // Grab all environment variables from the callee + let caller_env_vars = caller_stack.get_env_var_names(engine_state); + + // remove env vars that are present in the caller but not in the callee + // (the callee hid them) + for var in caller_env_vars.iter() { + if !callee_stack.has_env_var(engine_state, var) { + caller_stack.remove_env_var(engine_state, var); + } + } + + // add new env vars from callee to caller + for (var, value) in callee_stack.get_stack_env_vars() { + caller_stack.add_env_var(var, value); + } + + // set config to callee config, to capture any updates to that + caller_stack.config.clone_from(&callee_stack.config); +} + +fn eval_external( + engine_state: &EngineState, + stack: &mut Stack, + head: &Expression, + args: &[ExternalArgument], + input: PipelineData, +) -> Result { + let decl_id = engine_state + .find_decl("run-external".as_bytes(), &[]) + .ok_or(ShellError::ExternalNotSupported { + span: head.span(&engine_state), + })?; + + let command = engine_state.get_decl(decl_id); + + let mut call = Call::new(head.span(&engine_state)); + + call.add_positional(head.clone()); + + for arg in args { + match arg { + ExternalArgument::Regular(expr) => call.add_positional(expr.clone()), + ExternalArgument::Spread(expr) => call.add_spread(expr.clone()), + } + } + + command.run(engine_state, stack, &(&call).into(), input) +} + +pub fn eval_expression( + engine_state: &EngineState, + stack: &mut Stack, + expr: &Expression, +) -> Result { + let stack = &mut stack.start_collect_value(); + ::eval::(engine_state, stack, expr) +} + +/// Checks the expression to see if it's a internal or external call. If so, passes the input +/// into the call and gets out the result +/// Otherwise, invokes the expression +/// +/// It returns PipelineData with a boolean flag, indicating if the external failed to run. +/// The boolean flag **may only be true** for external calls, for internal calls, it always to be false. +pub fn eval_expression_with_input( + engine_state: &EngineState, + stack: &mut Stack, + expr: &Expression, + mut input: PipelineData, +) -> Result { + match &expr.expr { + Expr::Call(call) => { + input = eval_call::(engine_state, stack, call, input)?; + } + Expr::ExternalCall(head, args) => { + input = eval_external(engine_state, stack, head, args, input)?; + } + + Expr::Collect(var_id, expr) => { + input = eval_collect::(engine_state, stack, *var_id, expr, input)?; + } + + Expr::Subexpression(block_id) => { + let block = engine_state.get_block(*block_id); + // FIXME: protect this collect with ctrl-c + input = eval_subexpression::(engine_state, stack, block, input)?; + } + + Expr::FullCellPath(full_cell_path) => match &full_cell_path.head { + Expression { + expr: Expr::Subexpression(block_id), + span, + .. + } => { + let block = engine_state.get_block(*block_id); + + if !full_cell_path.tail.is_empty() { + let stack = &mut stack.start_collect_value(); + // FIXME: protect this collect with ctrl-c + input = eval_subexpression::(engine_state, stack, block, input)? + .into_value(*span)? + .follow_cell_path(&full_cell_path.tail)? + .into_owned() + .into_pipeline_data() + } else { + input = eval_subexpression::(engine_state, stack, block, input)?; + } + } + _ => { + input = eval_expression::(engine_state, stack, expr)?.into_pipeline_data(); + } + }, + + _ => { + input = eval_expression::(engine_state, stack, expr)?.into_pipeline_data(); + } + }; + + Ok(input) +} + +pub fn eval_block_with_early_return( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + input: PipelineData, +) -> Result { + match eval_block::(engine_state, stack, block, input) { + Err(ShellError::Return { span: _, value }) => Ok(PipelineData::Value(*value, None)), + x => x, + } +} + +pub fn eval_block( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + input: PipelineData, +) -> Result { + let result = eval_ir_block::(engine_state, stack, block, input); + if let Err(err) = &result { + stack.set_last_error(err); + } + result +} + +pub fn eval_collect( + engine_state: &EngineState, + stack: &mut Stack, + var_id: VarId, + expr: &Expression, + input: PipelineData, +) -> Result { + // Evaluate the expression with the variable set to the collected input + let span = input.span().unwrap_or(Span::unknown()); + + let metadata = match input.metadata() { + // Remove the `FilePath` metadata, because after `collect` it's no longer necessary to + // check where some input came from. + Some(PipelineMetadata { + data_source: DataSource::FilePath(_), + content_type: None, + }) => None, + other => other, + }; + + let input = input.into_value(span)?; + + stack.add_var(var_id, input.clone()); + + let result = eval_expression_with_input::( + engine_state, + stack, + expr, + // We still have to pass it as input + input.into_pipeline_data_with_metadata(metadata), + ); + + stack.remove_var(var_id); + + result +} + +pub fn eval_subexpression( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + input: PipelineData, +) -> Result { + eval_block::(engine_state, stack, block, input) +} + +pub fn eval_variable( + engine_state: &EngineState, + stack: &Stack, + var_id: VarId, + span: Span, +) -> Result { + match var_id { + // $nu + nu_protocol::NU_VARIABLE_ID => { + if let Some(val) = engine_state.get_constant(var_id) { + Ok(val.clone()) + } else { + Err(ShellError::VariableNotFoundAtRuntime { span }) + } + } + // $env + ENV_VARIABLE_ID => { + let env_vars = stack.get_env_vars(engine_state); + let env_columns = env_vars.keys(); + let env_values = env_vars.values(); + + let mut pairs = env_columns + .map(|x| x.to_string()) + .zip(env_values.cloned()) + .collect::>(); + + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + + Ok(Value::record(pairs.into_iter().collect(), span)) + } + var_id => stack.get_var(var_id, span), + } +} + +struct EvalRuntime; + +impl Eval for EvalRuntime { + type State<'a> = &'a EngineState; + + type MutState = Stack; + + fn get_config(engine_state: Self::State<'_>, stack: &mut Stack) -> Arc { + stack.get_config(engine_state) + } + + fn eval_var( + engine_state: &EngineState, + stack: &mut Stack, + var_id: VarId, + span: Span, + ) -> Result { + eval_variable(engine_state, stack, var_id, span) + } + + fn eval_call( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _: Span, + ) -> Result { + // FIXME: protect this collect with ctrl-c + eval_call::(engine_state, stack, call, PipelineData::empty())?.into_value(call.head) + } + + fn eval_external_call( + engine_state: &EngineState, + stack: &mut Stack, + head: &Expression, + args: &[ExternalArgument], + _: Span, + ) -> Result { + let span = head.span(&engine_state); + // FIXME: protect this collect with ctrl-c + eval_external(engine_state, stack, head, args, PipelineData::empty())?.into_value(span) + } + + fn eval_collect( + engine_state: &EngineState, + stack: &mut Stack, + var_id: VarId, + expr: &Expression, + ) -> Result { + // It's a little bizarre, but the expression can still have some kind of result even with + // nothing input + eval_collect::(engine_state, stack, var_id, expr, PipelineData::empty())? + .into_value(expr.span) + } + + fn eval_subexpression( + engine_state: &EngineState, + stack: &mut Stack, + block_id: BlockId, + span: Span, + ) -> Result { + let block = engine_state.get_block(block_id); + // FIXME: protect this collect with ctrl-c + eval_subexpression::(engine_state, stack, block, PipelineData::empty())?.into_value(span) + } + + fn regex_match( + engine_state: &EngineState, + op_span: Span, + lhs: &Value, + rhs: &Value, + invert: bool, + expr_span: Span, + ) -> Result { + lhs.regex_match(engine_state, op_span, rhs, invert, expr_span) + } + + fn eval_assignment( + engine_state: &EngineState, + stack: &mut Stack, + lhs: &Expression, + rhs: &Expression, + assignment: Assignment, + op_span: Span, + _expr_span: Span, + ) -> Result { + let rhs = eval_expression::(engine_state, stack, rhs)?; + + let rhs = match assignment { + Assignment::Assign => rhs, + Assignment::AddAssign => { + let lhs = eval_expression::(engine_state, stack, lhs)?; + lhs.add(op_span, &rhs, op_span)? + } + Assignment::SubtractAssign => { + let lhs = eval_expression::(engine_state, stack, lhs)?; + lhs.sub(op_span, &rhs, op_span)? + } + Assignment::MultiplyAssign => { + let lhs = eval_expression::(engine_state, stack, lhs)?; + lhs.mul(op_span, &rhs, op_span)? + } + Assignment::DivideAssign => { + let lhs = eval_expression::(engine_state, stack, lhs)?; + lhs.div(op_span, &rhs, op_span)? + } + Assignment::ConcatenateAssign => { + let lhs = eval_expression::(engine_state, stack, lhs)?; + lhs.concat(op_span, &rhs, op_span)? + } + }; + + match &lhs.expr { + Expr::Var(var_id) | Expr::VarDecl(var_id) => { + let var_info = engine_state.get_var(*var_id); + if var_info.mutable { + stack.add_var(*var_id, rhs); + Ok(Value::nothing(lhs.span(&engine_state))) + } else { + Err(ShellError::AssignmentRequiresMutableVar { + lhs_span: lhs.span(&engine_state), + }) + } + } + Expr::FullCellPath(cell_path) => { + match &cell_path.head.expr { + Expr::Var(var_id) | Expr::VarDecl(var_id) => { + // The $env variable is considered "mutable" in Nushell. + // As such, give it special treatment here. + let is_env = var_id == &ENV_VARIABLE_ID; + if is_env || engine_state.get_var(*var_id).mutable { + let mut lhs = + eval_expression::(engine_state, stack, &cell_path.head)?; + if is_env { + // Reject attempts to assign to the entire $env + if cell_path.tail.is_empty() { + return Err(ShellError::CannotReplaceEnv { + span: cell_path.head.span(&engine_state), + }); + } + + // Updating environment variables should be case-preserving, + // so we need to figure out the original key before we do anything. + let (key, span) = match &cell_path.tail[0] { + PathMember::String { val, span, .. } => (val.to_string(), span), + PathMember::Int { val, span, .. } => (val.to_string(), span), + }; + let original_key = if let Value::Record { val: record, .. } = &lhs { + record + .iter() + .rev() + .map(|(k, _)| k) + .find(|x| x.eq_ignore_case(&key)) + .cloned() + .unwrap_or(key) + } else { + key + }; + + // Retrieve the updated environment value. + lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?; + let value = lhs.follow_cell_path(&[{ + let mut pm = cell_path.tail[0].clone(); + pm.make_insensitive(); + pm + }])?; + + // Reject attempts to set automatic environment variables. + if is_automatic_env_var(&original_key) { + return Err(ShellError::AutomaticEnvVarSetManually { + envvar_name: original_key, + span: *span, + }); + } + + let is_config = original_key == "config"; + + stack.add_env_var(original_key, value.into_owned()); + + // Trigger the update to config, if we modified that. + if is_config { + stack.update_config(engine_state)?; + } + } else { + lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?; + stack.add_var(*var_id, lhs); + } + Ok(Value::nothing(cell_path.head.span(&engine_state))) + } else { + Err(ShellError::AssignmentRequiresMutableVar { + lhs_span: lhs.span(&engine_state), + }) + } + } + _ => Err(ShellError::AssignmentRequiresVar { + lhs_span: lhs.span(&engine_state), + }), + } + } + _ => Err(ShellError::AssignmentRequiresVar { + lhs_span: lhs.span(&engine_state), + }), + } + } + + fn eval_row_condition_or_closure( + engine_state: &EngineState, + stack: &mut Stack, + block_id: BlockId, + span: Span, + ) -> Result { + let captures = engine_state + .get_block(block_id) + .captures + .iter() + .map(|(id, span)| { + stack + .get_var(*id, *span) + .or_else(|_| { + engine_state + .get_var(*id) + .const_val + .clone() + .ok_or(ShellError::VariableNotFoundAtRuntime { span: *span }) + }) + .map(|var| (*id, var)) + }) + .collect::>()?; + + Ok(Value::closure(Closure { block_id, captures }, span)) + } + + fn eval_overlay(engine_state: &EngineState, span: Span) -> Result { + let name = String::from_utf8_lossy(engine_state.get_span_contents(span)).to_string(); + + Ok(Value::string(name, span)) + } + + fn unreachable(engine_state: &EngineState, expr: &Expression) -> Result { + Ok(Value::nothing(expr.span(&engine_state))) + } +} + +/// Returns whether a string, when used as the name of an environment variable, +/// is considered an automatic environment variable. +/// +/// An automatic environment variable cannot be assigned to by user code. +/// Current there are three of them: $env.PWD, $env.FILE_PWD, $env.CURRENT_FILE +pub(crate) fn is_automatic_env_var(var: &str) -> bool { + let names = ["PWD", "FILE_PWD", "CURRENT_FILE"]; + names.iter().any(|&name| { + if cfg!(windows) { + name.eq_ignore_case(var) + } else { + name.eq(var) + } + }) +} diff --git a/nushell/crates/nu-engine/src/eval_helpers.rs b/nushell/crates/nu-engine/src/eval_helpers.rs new file mode 100644 index 0000000..92b74d4 --- /dev/null +++ b/nushell/crates/nu-engine/src/eval_helpers.rs @@ -0,0 +1,93 @@ +use crate::{ + eval_block, eval_block_with_early_return, eval_expression, eval_expression_with_input, + eval_ir_block, eval_subexpression, +}; +use nu_protocol::{ + PipelineData, ShellError, Value, + ast::{Block, Expression}, + debugger::{WithDebug, WithoutDebug}, + engine::{EngineState, Stack}, +}; + +/// Type of eval_block() function +pub type EvalBlockFn = + fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; + +/// Type of eval_ir_block() function +pub type EvalIrBlockFn = + fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; + +/// Type of eval_block_with_early_return() function +pub type EvalBlockWithEarlyReturnFn = + fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; + +/// Type of eval_expression() function +pub type EvalExpressionFn = fn(&EngineState, &mut Stack, &Expression) -> Result; + +/// Type of eval_expression_with_input() function +pub type EvalExpressionWithInputFn = + fn(&EngineState, &mut Stack, &Expression, PipelineData) -> Result; + +/// Type of eval_subexpression() function +pub type EvalSubexpressionFn = + fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; + +/// Helper function to fetch `eval_block()` with the correct type parameter based on whether +/// engine_state is configured with or without a debugger. +pub fn get_eval_block(engine_state: &EngineState) -> EvalBlockFn { + if engine_state.is_debugging() { + eval_block:: + } else { + eval_block:: + } +} + +/// Helper function to fetch `eval_ir_block()` with the correct type parameter based on whether +/// engine_state is configured with or without a debugger. +pub fn get_eval_ir_block(engine_state: &EngineState) -> EvalIrBlockFn { + if engine_state.is_debugging() { + eval_ir_block:: + } else { + eval_ir_block:: + } +} + +/// Helper function to fetch `eval_block_with_early_return()` with the correct type parameter based +/// on whether engine_state is configured with or without a debugger. +pub fn get_eval_block_with_early_return(engine_state: &EngineState) -> EvalBlockWithEarlyReturnFn { + if engine_state.is_debugging() { + eval_block_with_early_return:: + } else { + eval_block_with_early_return:: + } +} + +/// Helper function to fetch `eval_expression()` with the correct type parameter based on whether +/// engine_state is configured with or without a debugger. +pub fn get_eval_expression(engine_state: &EngineState) -> EvalExpressionFn { + if engine_state.is_debugging() { + eval_expression:: + } else { + eval_expression:: + } +} + +/// Helper function to fetch `eval_expression_with_input()` with the correct type parameter based +/// on whether engine_state is configured with or without a debugger. +pub fn get_eval_expression_with_input(engine_state: &EngineState) -> EvalExpressionWithInputFn { + if engine_state.is_debugging() { + eval_expression_with_input:: + } else { + eval_expression_with_input:: + } +} + +/// Helper function to fetch `eval_subexpression()` with the correct type parameter based on whether +/// engine_state is configured with or without a debugger. +pub fn get_eval_subexpression(engine_state: &EngineState) -> EvalSubexpressionFn { + if engine_state.is_debugging() { + eval_subexpression:: + } else { + eval_subexpression:: + } +} diff --git a/nushell/crates/nu-engine/src/eval_ir.rs b/nushell/crates/nu-engine/src/eval_ir.rs new file mode 100644 index 0000000..b3c6d0d --- /dev/null +++ b/nushell/crates/nu-engine/src/eval_ir.rs @@ -0,0 +1,1616 @@ +use std::{borrow::Cow, fs::File, sync::Arc}; + +use nu_path::{expand_path, expand_path_with}; +use nu_protocol::{ + DataSource, DeclId, ENV_VARIABLE_ID, Flag, IntoPipelineData, IntoSpanned, ListStream, OutDest, + PipelineData, PipelineMetadata, PositionalArg, Range, Record, RegId, ShellError, Signals, + Signature, Span, Spanned, Type, Value, VarId, + ast::{Bits, Block, Boolean, CellPath, Comparison, Math, Operator}, + debugger::DebugContext, + engine::{ + Argument, Closure, EngineState, ErrorHandler, Matcher, Redirection, Stack, StateWorkingSet, + }, + ir::{Call, DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode}, + shell_error::io::IoError, +}; +use nu_utils::IgnoreCaseExt; + +use crate::{ + ENV_CONVERSIONS, convert_env_vars, eval::is_automatic_env_var, eval_block_with_early_return, +}; + +/// Evaluate the compiled representation of a [`Block`]. +pub fn eval_ir_block( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + input: PipelineData, +) -> Result { + // Rust does not check recursion limits outside of const evaluation. + // But nu programs run in the same process as the shell. + // To prevent a stack overflow in user code from crashing the shell, + // we limit the recursion depth of function calls. + let maximum_call_stack_depth: u64 = engine_state.config.recursion_limit as u64; + if stack.recursion_count > maximum_call_stack_depth { + return Err(ShellError::RecursionLimitReached { + recursion_limit: maximum_call_stack_depth, + span: block.span, + }); + } + + if let Some(ir_block) = &block.ir_block { + D::enter_block(engine_state, block); + + let args_base = stack.arguments.get_base(); + let error_handler_base = stack.error_handlers.get_base(); + + // Allocate and initialize registers. I've found that it's not really worth trying to avoid + // the heap allocation here by reusing buffers - our allocator is fast enough + let mut registers = Vec::with_capacity(ir_block.register_count as usize); + for _ in 0..ir_block.register_count { + registers.push(PipelineData::Empty); + } + + // Initialize file storage. + let mut files = vec![None; ir_block.file_count as usize]; + + let result = eval_ir_block_impl::( + &mut EvalContext { + engine_state, + stack, + data: &ir_block.data, + block_span: &block.span, + args_base, + error_handler_base, + redirect_out: None, + redirect_err: None, + matches: vec![], + registers: &mut registers[..], + files: &mut files[..], + }, + ir_block, + input, + ); + + stack.error_handlers.leave_frame(error_handler_base); + stack.arguments.leave_frame(args_base); + + D::leave_block(engine_state, block); + + result + } else { + // FIXME blocks having IR should not be optional + Err(ShellError::GenericError { + error: "Can't evaluate block in IR mode".into(), + msg: "block is missing compiled representation".into(), + span: block.span, + help: Some("the IrBlock is probably missing due to a compilation error".into()), + inner: vec![], + }) + } +} + +/// All of the pointers necessary for evaluation +struct EvalContext<'a> { + engine_state: &'a EngineState, + stack: &'a mut Stack, + data: &'a Arc<[u8]>, + /// The span of the block + block_span: &'a Option, + /// Base index on the argument stack to reset to after a call + args_base: usize, + /// Base index on the error handler stack to reset to after a call + error_handler_base: usize, + /// State set by redirect-out + redirect_out: Option, + /// State set by redirect-err + redirect_err: Option, + /// Scratch space to use for `match` + matches: Vec<(VarId, Value)>, + /// Intermediate pipeline data storage used by instructions, indexed by RegId + registers: &'a mut [PipelineData], + /// Holds open files used by redirections + files: &'a mut [Option>], +} + +impl<'a> EvalContext<'a> { + /// Replace the contents of a register with a new value + #[inline] + fn put_reg(&mut self, reg_id: RegId, new_value: PipelineData) { + // log::trace!("{reg_id} <- {new_value:?}"); + self.registers[reg_id.get() as usize] = new_value; + } + + /// Borrow the contents of a register. + #[inline] + fn borrow_reg(&self, reg_id: RegId) -> &PipelineData { + &self.registers[reg_id.get() as usize] + } + + /// Replace the contents of a register with `Empty` and then return the value that it contained + #[inline] + fn take_reg(&mut self, reg_id: RegId) -> PipelineData { + // log::trace!("<- {reg_id}"); + std::mem::replace( + &mut self.registers[reg_id.get() as usize], + PipelineData::Empty, + ) + } + + /// Clone data from a register. Must be collected first. + fn clone_reg(&mut self, reg_id: RegId, error_span: Span) -> Result { + match &self.registers[reg_id.get() as usize] { + PipelineData::Empty => Ok(PipelineData::Empty), + PipelineData::Value(val, meta) => Ok(PipelineData::Value(val.clone(), meta.clone())), + _ => Err(ShellError::IrEvalError { + msg: "Must collect to value before using instruction that clones from a register" + .into(), + span: Some(error_span), + }), + } + } + + /// Clone a value from a register. Must be collected first. + fn clone_reg_value(&mut self, reg_id: RegId, fallback_span: Span) -> Result { + match self.clone_reg(reg_id, fallback_span)? { + PipelineData::Empty => Ok(Value::nothing(fallback_span)), + PipelineData::Value(val, _) => Ok(val), + _ => unreachable!("clone_reg should never return stream data"), + } + } + + /// Take and implicitly collect a register to a value + fn collect_reg(&mut self, reg_id: RegId, fallback_span: Span) -> Result { + let data = self.take_reg(reg_id); + let span = data.span().unwrap_or(fallback_span); + data.into_value(span) + } + + /// Get a string from data or produce evaluation error if it's invalid UTF-8 + fn get_str(&self, slice: DataSlice, error_span: Span) -> Result<&'a str, ShellError> { + std::str::from_utf8(&self.data[slice]).map_err(|_| ShellError::IrEvalError { + msg: format!("data slice does not refer to valid UTF-8: {slice:?}"), + span: Some(error_span), + }) + } +} + +/// Eval an IR block on the provided slice of registers. +fn eval_ir_block_impl( + ctx: &mut EvalContext<'_>, + ir_block: &IrBlock, + input: PipelineData, +) -> Result { + if !ctx.registers.is_empty() { + ctx.registers[0] = input; + } + + // Program counter, starts at zero. + let mut pc = 0; + let need_backtrace = ctx.engine_state.get_env_var("NU_BACKTRACE").is_some(); + + while pc < ir_block.instructions.len() { + let instruction = &ir_block.instructions[pc]; + let span = &ir_block.spans[pc]; + let ast = &ir_block.ast[pc]; + + D::enter_instruction(ctx.engine_state, ir_block, pc, ctx.registers); + + let result = eval_instruction::(ctx, instruction, span, ast, need_backtrace); + + D::leave_instruction( + ctx.engine_state, + ir_block, + pc, + ctx.registers, + result.as_ref().err(), + ); + + match result { + Ok(InstructionResult::Continue) => { + pc += 1; + } + Ok(InstructionResult::Branch(next_pc)) => { + pc = next_pc; + } + Ok(InstructionResult::Return(reg_id)) => { + return Ok(ctx.take_reg(reg_id)); + } + Err( + err @ (ShellError::Return { .. } + | ShellError::Continue { .. } + | ShellError::Break { .. }), + ) => { + // These block control related errors should be passed through + return Err(err); + } + Err(err) => { + if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) { + // If an error handler is set, branch there + prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span))); + pc = error_handler.handler_index; + } else if need_backtrace { + let err = ShellError::into_chainned(err, *span); + return Err(err); + } else { + return Err(err); + } + } + } + } + + // Fell out of the loop, without encountering a Return. + Err(ShellError::IrEvalError { + msg: format!( + "Program counter out of range (pc={pc}, len={len})", + len = ir_block.instructions.len(), + ), + span: *ctx.block_span, + }) +} + +/// Prepare the context for an error handler +fn prepare_error_handler( + ctx: &mut EvalContext<'_>, + error_handler: ErrorHandler, + error: Option>, +) { + if let Some(reg_id) = error_handler.error_register { + if let Some(error) = error { + // Stack state has to be updated for stuff like LAST_EXIT_CODE + ctx.stack.set_last_error(&error.item); + // Create the error value and put it in the register + ctx.put_reg( + reg_id, + error + .item + .into_value(&StateWorkingSet::new(ctx.engine_state), error.span) + .into_pipeline_data(), + ); + } else { + // Set the register to empty + ctx.put_reg(reg_id, PipelineData::Empty); + } + } +} + +/// The result of performing an instruction. Describes what should happen next +#[derive(Debug)] +enum InstructionResult { + Continue, + Branch(usize), + Return(RegId), +} + +/// Perform an instruction +fn eval_instruction( + ctx: &mut EvalContext<'_>, + instruction: &Instruction, + span: &Span, + ast: &Option, + need_backtrace: bool, +) -> Result { + use self::InstructionResult::*; + + // See the docs for `Instruction` for more information on what these instructions are supposed + // to do. + match instruction { + Instruction::Unreachable => Err(ShellError::IrEvalError { + msg: "Reached unreachable code".into(), + span: Some(*span), + }), + Instruction::LoadLiteral { dst, lit } => load_literal(ctx, *dst, lit, *span), + Instruction::LoadValue { dst, val } => { + ctx.put_reg(*dst, Value::clone(val).into_pipeline_data()); + Ok(Continue) + } + Instruction::Move { dst, src } => { + let val = ctx.take_reg(*src); + ctx.put_reg(*dst, val); + Ok(Continue) + } + Instruction::Clone { dst, src } => { + let data = ctx.clone_reg(*src, *span)?; + ctx.put_reg(*dst, data); + Ok(Continue) + } + Instruction::Collect { src_dst } => { + let data = ctx.take_reg(*src_dst); + let value = collect(data, *span)?; + ctx.put_reg(*src_dst, value); + Ok(Continue) + } + Instruction::Span { src_dst } => { + let data = ctx.take_reg(*src_dst); + let spanned = data.with_span(*span); + ctx.put_reg(*src_dst, spanned); + Ok(Continue) + } + Instruction::Drop { src } => { + ctx.take_reg(*src); + Ok(Continue) + } + Instruction::Drain { src } => { + let data = ctx.take_reg(*src); + drain(ctx, data) + } + Instruction::DrainIfEnd { src } => { + let data = ctx.take_reg(*src); + let res = { + let stack = &mut ctx + .stack + .push_redirection(ctx.redirect_out.clone(), ctx.redirect_err.clone()); + data.drain_to_out_dests(ctx.engine_state, stack)? + }; + ctx.put_reg(*src, res); + Ok(Continue) + } + Instruction::LoadVariable { dst, var_id } => { + let value = get_var(ctx, *var_id, *span)?; + ctx.put_reg(*dst, value.into_pipeline_data()); + Ok(Continue) + } + Instruction::StoreVariable { var_id, src } => { + let value = ctx.collect_reg(*src, *span)?; + ctx.stack.add_var(*var_id, value); + Ok(Continue) + } + Instruction::DropVariable { var_id } => { + ctx.stack.remove_var(*var_id); + Ok(Continue) + } + Instruction::LoadEnv { dst, key } => { + let key = ctx.get_str(*key, *span)?; + if let Some(value) = get_env_var_case_insensitive(ctx, key) { + let new_value = value.clone().into_pipeline_data(); + ctx.put_reg(*dst, new_value); + Ok(Continue) + } else { + // FIXME: using the same span twice, shouldn't this really be + // EnvVarNotFoundAtRuntime? There are tests that depend on CantFindColumn though... + Err(ShellError::CantFindColumn { + col_name: key.into(), + span: Some(*span), + src_span: *span, + }) + } + } + Instruction::LoadEnvOpt { dst, key } => { + let key = ctx.get_str(*key, *span)?; + let value = get_env_var_case_insensitive(ctx, key) + .cloned() + .unwrap_or(Value::nothing(*span)); + ctx.put_reg(*dst, value.into_pipeline_data()); + Ok(Continue) + } + Instruction::StoreEnv { key, src } => { + let key = ctx.get_str(*key, *span)?; + let value = ctx.collect_reg(*src, *span)?; + + let key = get_env_var_name_case_insensitive(ctx, key); + + if !is_automatic_env_var(&key) { + let is_config = key == "config"; + let update_conversions = key == ENV_CONVERSIONS; + + ctx.stack.add_env_var(key.into_owned(), value.clone()); + + if is_config { + ctx.stack.update_config(ctx.engine_state)?; + } + if update_conversions { + convert_env_vars(ctx.stack, ctx.engine_state, &value)?; + } + Ok(Continue) + } else { + Err(ShellError::AutomaticEnvVarSetManually { + envvar_name: key.into(), + span: *span, + }) + } + } + Instruction::PushPositional { src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + ctx.stack.arguments.push(Argument::Positional { + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::AppendRest { src } => { + let vals = ctx.collect_reg(*src, *span)?.with_span(*span); + ctx.stack.arguments.push(Argument::Spread { + span: *span, + vals, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushFlag { name } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Flag { + data, + name: *name, + short: DataSlice::empty(), + span: *span, + }); + Ok(Continue) + } + Instruction::PushShortFlag { short } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Flag { + data, + name: DataSlice::empty(), + short: *short, + span: *span, + }); + Ok(Continue) + } + Instruction::PushNamed { name, src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Named { + data, + name: *name, + short: DataSlice::empty(), + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushShortNamed { short, src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Named { + data, + name: DataSlice::empty(), + short: *short, + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushParserInfo { name, info } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::ParserInfo { + data, + name: *name, + info: info.clone(), + }); + Ok(Continue) + } + Instruction::RedirectOut { mode } => { + ctx.redirect_out = eval_redirection(ctx, mode, *span, RedirectionStream::Out)?; + Ok(Continue) + } + Instruction::RedirectErr { mode } => { + ctx.redirect_err = eval_redirection(ctx, mode, *span, RedirectionStream::Err)?; + Ok(Continue) + } + Instruction::CheckErrRedirected { src } => match ctx.borrow_reg(*src) { + #[cfg(feature = "os")] + PipelineData::ByteStream(stream, _) + if matches!(stream.source(), nu_protocol::ByteStreamSource::Child(_)) => + { + Ok(Continue) + } + _ => Err(ShellError::GenericError { + error: "Can't redirect stderr of internal command output".into(), + msg: "piping stderr only works on external commands".into(), + span: Some(*span), + help: None, + inner: vec![], + }), + }, + Instruction::OpenFile { + file_num, + path, + append, + } => { + let path = ctx.collect_reg(*path, *span)?; + let file = open_file(ctx, &path, *append)?; + ctx.files[*file_num as usize] = Some(file); + Ok(Continue) + } + Instruction::WriteFile { file_num, src } => { + let src = ctx.take_reg(*src); + let file = ctx + .files + .get(*file_num as usize) + .cloned() + .flatten() + .ok_or_else(|| ShellError::IrEvalError { + msg: format!("Tried to write to file #{file_num}, but it is not open"), + span: Some(*span), + })?; + let is_external = if let PipelineData::ByteStream(stream, ..) = &src { + stream.source().is_external() + } else { + false + }; + if let Err(err) = src.write_to(file.as_ref()) { + if is_external { + ctx.stack.set_last_error(&err); + } + Err(err)? + } else { + Ok(Continue) + } + } + Instruction::CloseFile { file_num } => { + if ctx.files[*file_num as usize].take().is_some() { + Ok(Continue) + } else { + Err(ShellError::IrEvalError { + msg: format!("Tried to close file #{file_num}, but it is not open"), + span: Some(*span), + }) + } + } + Instruction::Call { decl_id, src_dst } => { + let input = ctx.take_reg(*src_dst); + let mut result = eval_call::(ctx, *decl_id, *span, input)?; + if need_backtrace { + match &mut result { + PipelineData::ByteStream(s, ..) => s.push_caller_span(*span), + PipelineData::ListStream(s, ..) => s.push_caller_span(*span), + _ => (), + }; + } + ctx.put_reg(*src_dst, result); + Ok(Continue) + } + Instruction::StringAppend { src_dst, val } => { + let string_value = ctx.collect_reg(*src_dst, *span)?; + let operand_value = ctx.collect_reg(*val, *span)?; + let string_span = string_value.span(); + + let mut string = string_value.into_string()?; + let operand = if let Value::String { val, .. } = operand_value { + // Small optimization, so we don't have to copy the string *again* + val + } else { + operand_value.to_expanded_string(", ", ctx.engine_state.get_config()) + }; + string.push_str(&operand); + + let new_string_value = Value::string(string, string_span); + ctx.put_reg(*src_dst, new_string_value.into_pipeline_data()); + Ok(Continue) + } + Instruction::GlobFrom { src_dst, no_expand } => { + let string_value = ctx.collect_reg(*src_dst, *span)?; + let glob_value = if matches!(string_value, Value::Glob { .. }) { + // It already is a glob, so don't touch it. + string_value + } else { + // Treat it as a string, then cast + let string = string_value.into_string()?; + Value::glob(string, *no_expand, *span) + }; + ctx.put_reg(*src_dst, glob_value.into_pipeline_data()); + Ok(Continue) + } + Instruction::ListPush { src_dst, item } => { + let list_value = ctx.collect_reg(*src_dst, *span)?; + let item = ctx.collect_reg(*item, *span)?; + let list_span = list_value.span(); + let mut list = list_value.into_list()?; + list.push(item); + ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data()); + Ok(Continue) + } + Instruction::ListSpread { src_dst, items } => { + let list_value = ctx.collect_reg(*src_dst, *span)?; + let items = ctx.collect_reg(*items, *span)?; + let list_span = list_value.span(); + let items_span = items.span(); + let mut list = list_value.into_list()?; + list.extend( + items + .into_list() + .map_err(|_| ShellError::CannotSpreadAsList { span: items_span })?, + ); + ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data()); + Ok(Continue) + } + Instruction::RecordInsert { src_dst, key, val } => { + let record_value = ctx.collect_reg(*src_dst, *span)?; + let key = ctx.collect_reg(*key, *span)?; + let val = ctx.collect_reg(*val, *span)?; + let record_span = record_value.span(); + let mut record = record_value.into_record()?; + + let key = key.coerce_into_string()?; + if let Some(old_value) = record.insert(&key, val) { + return Err(ShellError::ColumnDefinedTwice { + col_name: key, + second_use: *span, + first_use: old_value.span(), + }); + } + + ctx.put_reg( + *src_dst, + Value::record(record, record_span).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::RecordSpread { src_dst, items } => { + let record_value = ctx.collect_reg(*src_dst, *span)?; + let items = ctx.collect_reg(*items, *span)?; + let record_span = record_value.span(); + let items_span = items.span(); + let mut record = record_value.into_record()?; + // Not using .extend() here because it doesn't handle duplicates + for (key, val) in items + .into_record() + .map_err(|_| ShellError::CannotSpreadAsRecord { span: items_span })? + { + if let Some(first_value) = record.insert(&key, val) { + return Err(ShellError::ColumnDefinedTwice { + col_name: key, + second_use: *span, + first_use: first_value.span(), + }); + } + } + ctx.put_reg( + *src_dst, + Value::record(record, record_span).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::Not { src_dst } => { + let bool = ctx.collect_reg(*src_dst, *span)?; + let negated = !bool.as_bool()?; + ctx.put_reg( + *src_dst, + Value::bool(negated, bool.span()).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::BinaryOp { lhs_dst, op, rhs } => binary_op(ctx, *lhs_dst, op, *rhs, *span), + Instruction::FollowCellPath { src_dst, path } => { + let data = ctx.take_reg(*src_dst); + let path = ctx.take_reg(*path); + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + let value = data.follow_cell_path(&path.members, *span)?; + ctx.put_reg(*src_dst, value.into_pipeline_data()); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::CloneCellPath { dst, src, path } => { + let value = ctx.clone_reg_value(*src, *span)?; + let path = ctx.take_reg(*path); + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + let value = value.follow_cell_path(&path.members)?; + ctx.put_reg(*dst, value.into_owned().into_pipeline_data()); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::UpsertCellPath { + src_dst, + path, + new_value, + } => { + let data = ctx.take_reg(*src_dst); + let metadata = data.metadata(); + // Change the span because we're modifying it + let mut value = data.into_value(*span)?; + let path = ctx.take_reg(*path); + let new_value = ctx.collect_reg(*new_value, *span)?; + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + value.upsert_data_at_cell_path(&path.members, new_value)?; + ctx.put_reg(*src_dst, value.into_pipeline_data_with_metadata(metadata)); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::Jump { index } => Ok(Branch(*index)), + Instruction::BranchIf { cond, index } => { + let data = ctx.take_reg(*cond); + let data_span = data.span(); + let val = match data { + PipelineData::Value(Value::Bool { val, .. }, _) => val, + PipelineData::Value(Value::Error { error, .. }, _) => { + return Err(*error); + } + _ => { + return Err(ShellError::TypeMismatch { + err_message: "expected bool".into(), + span: data_span.unwrap_or(*span), + }); + } + }; + if val { + Ok(Branch(*index)) + } else { + Ok(Continue) + } + } + Instruction::BranchIfEmpty { src, index } => { + let is_empty = matches!( + ctx.borrow_reg(*src), + PipelineData::Empty | PipelineData::Value(Value::Nothing { .. }, _) + ); + + if is_empty { + Ok(Branch(*index)) + } else { + Ok(Continue) + } + } + Instruction::Match { + pattern, + src, + index, + } => { + let value = ctx.clone_reg_value(*src, *span)?; + ctx.matches.clear(); + if pattern.match_value(&value, &mut ctx.matches) { + // Match succeeded: set variables and branch + for (var_id, match_value) in ctx.matches.drain(..) { + ctx.stack.add_var(var_id, match_value); + } + Ok(Branch(*index)) + } else { + // Failed to match, put back original value + ctx.matches.clear(); + Ok(Continue) + } + } + Instruction::CheckMatchGuard { src } => { + if matches!( + ctx.borrow_reg(*src), + PipelineData::Value(Value::Bool { .. }, _) + ) { + Ok(Continue) + } else { + Err(ShellError::MatchGuardNotBool { span: *span }) + } + } + Instruction::Iterate { + dst, + stream, + end_index, + } => eval_iterate(ctx, *dst, *stream, *end_index), + Instruction::OnError { index } => { + ctx.stack.error_handlers.push(ErrorHandler { + handler_index: *index, + error_register: None, + }); + Ok(Continue) + } + Instruction::OnErrorInto { index, dst } => { + ctx.stack.error_handlers.push(ErrorHandler { + handler_index: *index, + error_register: Some(*dst), + }); + Ok(Continue) + } + Instruction::PopErrorHandler => { + ctx.stack.error_handlers.pop(ctx.error_handler_base); + Ok(Continue) + } + Instruction::ReturnEarly { src } => { + let val = ctx.collect_reg(*src, *span)?; + Err(ShellError::Return { + span: *span, + value: Box::new(val), + }) + } + Instruction::Return { src } => Ok(Return(*src)), + } +} + +/// Load a literal value into a register +fn load_literal( + ctx: &mut EvalContext<'_>, + dst: RegId, + lit: &Literal, + span: Span, +) -> Result { + let value = literal_value(ctx, lit, span)?; + ctx.put_reg(dst, PipelineData::Value(value, None)); + Ok(InstructionResult::Continue) +} + +fn literal_value( + ctx: &mut EvalContext<'_>, + lit: &Literal, + span: Span, +) -> Result { + Ok(match lit { + Literal::Bool(b) => Value::bool(*b, span), + Literal::Int(i) => Value::int(*i, span), + Literal::Float(f) => Value::float(*f, span), + Literal::Filesize(q) => Value::filesize(*q, span), + Literal::Duration(q) => Value::duration(*q, span), + Literal::Binary(bin) => Value::binary(&ctx.data[*bin], span), + Literal::Block(block_id) | Literal::RowCondition(block_id) | Literal::Closure(block_id) => { + let block = ctx.engine_state.get_block(*block_id); + let captures = block + .captures + .iter() + .map(|(var_id, span)| get_var(ctx, *var_id, *span).map(|val| (*var_id, val))) + .collect::, ShellError>>()?; + Value::closure( + Closure { + block_id: *block_id, + captures, + }, + span, + ) + } + Literal::Range { + start, + step, + end, + inclusion, + } => { + let start = ctx.collect_reg(*start, span)?; + let step = ctx.collect_reg(*step, span)?; + let end = ctx.collect_reg(*end, span)?; + let range = Range::new(start, step, end, *inclusion, span)?; + Value::range(range, span) + } + Literal::List { capacity } => Value::list(Vec::with_capacity(*capacity), span), + Literal::Record { capacity } => Value::record(Record::with_capacity(*capacity), span), + Literal::Filepath { + val: path, + no_expand, + } => { + let path = ctx.get_str(*path, span)?; + if *no_expand { + Value::string(path, span) + } else { + let path = expand_path(path, true); + Value::string(path.to_string_lossy(), span) + } + } + Literal::Directory { + val: path, + no_expand, + } => { + let path = ctx.get_str(*path, span)?; + if path == "-" { + Value::string("-", span) + } else if *no_expand { + Value::string(path, span) + } else { + let path = expand_path(path, true); + Value::string(path.to_string_lossy(), span) + } + } + Literal::GlobPattern { val, no_expand } => { + Value::glob(ctx.get_str(*val, span)?, *no_expand, span) + } + Literal::String(s) => Value::string(ctx.get_str(*s, span)?, span), + Literal::RawString(s) => Value::string(ctx.get_str(*s, span)?, span), + Literal::CellPath(path) => Value::cell_path(CellPath::clone(path), span), + Literal::Date(dt) => Value::date(**dt, span), + Literal::Nothing => Value::nothing(span), + }) +} + +fn binary_op( + ctx: &mut EvalContext<'_>, + lhs_dst: RegId, + op: &Operator, + rhs: RegId, + span: Span, +) -> Result { + let lhs_val = ctx.collect_reg(lhs_dst, span)?; + let rhs_val = ctx.collect_reg(rhs, span)?; + + // Handle binary op errors early + if let Value::Error { error, .. } = lhs_val { + return Err(*error); + } + if let Value::Error { error, .. } = rhs_val { + return Err(*error); + } + + // We only have access to one span here, but the generated code usually adds a `span` + // instruction to set the output span to the right span. + let op_span = span; + + let result = match op { + Operator::Comparison(cmp) => match cmp { + Comparison::Equal => lhs_val.eq(op_span, &rhs_val, span)?, + Comparison::NotEqual => lhs_val.ne(op_span, &rhs_val, span)?, + Comparison::LessThan => lhs_val.lt(op_span, &rhs_val, span)?, + Comparison::GreaterThan => lhs_val.gt(op_span, &rhs_val, span)?, + Comparison::LessThanOrEqual => lhs_val.lte(op_span, &rhs_val, span)?, + Comparison::GreaterThanOrEqual => lhs_val.gte(op_span, &rhs_val, span)?, + Comparison::RegexMatch => { + lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, false, span)? + } + Comparison::NotRegexMatch => { + lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, true, span)? + } + Comparison::In => lhs_val.r#in(op_span, &rhs_val, span)?, + Comparison::NotIn => lhs_val.not_in(op_span, &rhs_val, span)?, + Comparison::Has => lhs_val.has(op_span, &rhs_val, span)?, + Comparison::NotHas => lhs_val.not_has(op_span, &rhs_val, span)?, + Comparison::StartsWith => lhs_val.starts_with(op_span, &rhs_val, span)?, + Comparison::EndsWith => lhs_val.ends_with(op_span, &rhs_val, span)?, + }, + Operator::Math(mat) => match mat { + Math::Add => lhs_val.add(op_span, &rhs_val, span)?, + Math::Subtract => lhs_val.sub(op_span, &rhs_val, span)?, + Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?, + Math::Divide => lhs_val.div(op_span, &rhs_val, span)?, + Math::FloorDivide => lhs_val.floor_div(op_span, &rhs_val, span)?, + Math::Modulo => lhs_val.modulo(op_span, &rhs_val, span)?, + Math::Pow => lhs_val.pow(op_span, &rhs_val, span)?, + Math::Concatenate => lhs_val.concat(op_span, &rhs_val, span)?, + }, + Operator::Boolean(bl) => match bl { + Boolean::Or => lhs_val.or(op_span, &rhs_val, span)?, + Boolean::Xor => lhs_val.xor(op_span, &rhs_val, span)?, + Boolean::And => lhs_val.and(op_span, &rhs_val, span)?, + }, + Operator::Bits(bit) => match bit { + Bits::BitOr => lhs_val.bit_or(op_span, &rhs_val, span)?, + Bits::BitXor => lhs_val.bit_xor(op_span, &rhs_val, span)?, + Bits::BitAnd => lhs_val.bit_and(op_span, &rhs_val, span)?, + Bits::ShiftLeft => lhs_val.bit_shl(op_span, &rhs_val, span)?, + Bits::ShiftRight => lhs_val.bit_shr(op_span, &rhs_val, span)?, + }, + Operator::Assignment(_asg) => { + return Err(ShellError::IrEvalError { + msg: "can't eval assignment with the `binary-op` instruction".into(), + span: Some(span), + }); + } + }; + + ctx.put_reg(lhs_dst, PipelineData::Value(result, None)); + + Ok(InstructionResult::Continue) +} + +/// Evaluate a call +fn eval_call( + ctx: &mut EvalContext<'_>, + decl_id: DeclId, + head: Span, + input: PipelineData, +) -> Result { + let EvalContext { + engine_state, + stack: caller_stack, + args_base, + redirect_out, + redirect_err, + .. + } = ctx; + + let args_len = caller_stack.arguments.get_len(*args_base); + let decl = engine_state.get_decl(decl_id); + + // Set up redirect modes + let mut caller_stack = caller_stack.push_redirection(redirect_out.take(), redirect_err.take()); + + let result = (|| { + if let Some(block_id) = decl.block_id() { + // If the decl is a custom command + let block = engine_state.get_block(block_id); + + // check types after acquiring block to avoid unnecessarily cloning Signature + check_input_types(&input, &block.signature, head)?; + + // Set up a callee stack with the captures and move arguments from the stack into variables + let mut callee_stack = caller_stack.gather_captures(engine_state, &block.captures); + + gather_arguments( + engine_state, + block, + &mut caller_stack, + &mut callee_stack, + *args_base, + args_len, + head, + )?; + + // Add one to the recursion count, so we don't recurse too deep. Stack overflows are not + // recoverable in Rust. + callee_stack.recursion_count += 1; + + let result = + eval_block_with_early_return::(engine_state, &mut callee_stack, block, input); + + // Move environment variables back into the caller stack scope if requested to do so + if block.redirect_env { + redirect_env(engine_state, &mut caller_stack, &callee_stack); + } + + result + } else { + check_input_types(&input, &decl.signature(), head)?; + // FIXME: precalculate this and save it somewhere + let span = Span::merge_many( + std::iter::once(head).chain( + caller_stack + .arguments + .get_args(*args_base, args_len) + .iter() + .flat_map(|arg| arg.span()), + ), + ); + + let call = Call { + decl_id, + head, + span, + args_base: *args_base, + args_len, + }; + + // Run the call + decl.run(engine_state, &mut caller_stack, &(&call).into(), input) + } + })(); + + drop(caller_stack); + + // Important that this runs, to reset state post-call: + ctx.stack.arguments.leave_frame(ctx.args_base); + ctx.redirect_out = None; + ctx.redirect_err = None; + + result +} + +fn find_named_var_id( + sig: &Signature, + name: &[u8], + short: &[u8], + span: Span, +) -> Result { + sig.named + .iter() + .find(|n| { + if !n.long.is_empty() { + n.long.as_bytes() == name + } else { + // It's possible to only have a short name and no long name + n.short + .is_some_and(|s| s.encode_utf8(&mut [0; 4]).as_bytes() == short) + } + }) + .ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block does not have an argument named `{}`", + String::from_utf8_lossy(name) + ), + span: Some(span), + }) + .and_then(|flag| expect_named_var_id(flag, span)) +} + +fn expect_named_var_id(arg: &Flag, span: Span) -> Result { + arg.var_id.ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block signature is missing var id for named arg `{}`", + arg.long + ), + span: Some(span), + }) +} + +fn expect_positional_var_id(arg: &PositionalArg, span: Span) -> Result { + arg.var_id.ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block signature is missing var id for positional arg `{}`", + arg.name + ), + span: Some(span), + }) +} + +/// Move arguments from the stack into variables for a custom command +fn gather_arguments( + engine_state: &EngineState, + block: &Block, + caller_stack: &mut Stack, + callee_stack: &mut Stack, + args_base: usize, + args_len: usize, + call_head: Span, +) -> Result<(), ShellError> { + let mut positional_iter = block + .signature + .required_positional + .iter() + .map(|p| (p, true)) + .chain( + block + .signature + .optional_positional + .iter() + .map(|p| (p, false)), + ); + + // Arguments that didn't get consumed by required/optional + let mut rest = vec![]; + + // If we encounter a spread, all further positionals should go to rest + let mut always_spread = false; + + for arg in caller_stack.arguments.drain_args(args_base, args_len) { + match arg { + Argument::Positional { span, val, .. } => { + // Don't check next positional arg if we encountered a spread previously + let next = (!always_spread).then(|| positional_iter.next()).flatten(); + if let Some((positional_arg, required)) = next { + let var_id = expect_positional_var_id(positional_arg, span)?; + if required { + // By checking the type of the bound variable rather than converting the + // SyntaxShape here, we might be able to save some allocations and effort + let variable = engine_state.get_var(var_id); + check_type(&val, &variable.ty)?; + } + callee_stack.add_var(var_id, val); + } else { + rest.push(val); + } + } + Argument::Spread { vals, .. } => { + if let Value::List { vals, .. } = vals { + rest.extend(vals); + // All further positional args should go to spread + always_spread = true; + } else if let Value::Error { error, .. } = vals { + return Err(*error); + } else { + return Err(ShellError::CannotSpreadAsList { span: vals.span() }); + } + } + Argument::Flag { + data, + name, + short, + span, + } => { + let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?; + callee_stack.add_var(var_id, Value::bool(true, span)) + } + Argument::Named { + data, + name, + short, + span, + val, + .. + } => { + let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?; + callee_stack.add_var(var_id, val) + } + Argument::ParserInfo { .. } => (), + } + } + + // Add the collected rest of the arguments if a spread argument exists + if let Some(rest_arg) = &block.signature.rest_positional { + let rest_span = rest.first().map(|v| v.span()).unwrap_or(call_head); + let var_id = expect_positional_var_id(rest_arg, rest_span)?; + callee_stack.add_var(var_id, Value::list(rest, rest_span)); + } + + // Check for arguments that haven't yet been set and set them to their defaults + for (positional_arg, _) in positional_iter { + let var_id = expect_positional_var_id(positional_arg, call_head)?; + callee_stack.add_var( + var_id, + positional_arg + .default_value + .clone() + .unwrap_or(Value::nothing(call_head)), + ); + } + + for named_arg in &block.signature.named { + if let Some(var_id) = named_arg.var_id { + // For named arguments, we do this check by looking to see if the variable was set yet on + // the stack. This assumes that the stack's variables was previously empty, but that's a + // fair assumption for a brand new callee stack. + if !callee_stack.vars.iter().any(|(id, _)| *id == var_id) { + let val = if named_arg.arg.is_none() { + Value::bool(false, call_head) + } else if let Some(value) = &named_arg.default_value { + value.clone() + } else { + Value::nothing(call_head) + }; + callee_stack.add_var(var_id, val); + } + } + } + + Ok(()) +} + +/// Type check helper. Produces `CantConvert` error if `val` is not compatible with `ty`. +fn check_type(val: &Value, ty: &Type) -> Result<(), ShellError> { + match val { + Value::Error { error, .. } => Err(*error.clone()), + _ if val.is_subtype_of(ty) => Ok(()), + _ => Err(ShellError::CantConvert { + to_type: ty.to_string(), + from_type: val.get_type().to_string(), + span: val.span(), + help: None, + }), + } +} + +/// Type check pipeline input against command's input types +fn check_input_types( + input: &PipelineData, + signature: &Signature, + head: Span, +) -> Result<(), ShellError> { + let io_types = &signature.input_output_types; + + // If a command doesn't have any input/output types, then treat command input type as any + if io_types.is_empty() { + return Ok(()); + } + + // If a command only has a nothing input type, then allow any input data + if io_types.iter().all(|(intype, _)| intype == &Type::Nothing) { + return Ok(()); + } + + match input { + // early return error directly if detected + PipelineData::Value(Value::Error { error, .. }, ..) => return Err(*error.clone()), + // bypass run-time typechecking for custom types + PipelineData::Value(Value::Custom { .. }, ..) => return Ok(()), + _ => (), + } + + // Check if the input type is compatible with *any* of the command's possible input types + if io_types + .iter() + .any(|(command_type, _)| input.is_subtype_of(command_type)) + { + return Ok(()); + } + + let mut input_types = io_types + .iter() + .map(|(input, _)| input.to_string()) + .collect::>(); + + let expected_string = match input_types.len() { + 0 => { + return Err(ShellError::NushellFailed { + msg: "Command input type strings is empty, despite being non-zero earlier" + .to_string(), + }); + } + 1 => input_types.swap_remove(0), + 2 => input_types.join(" and "), + _ => { + input_types + .last_mut() + .expect("Vector with length >2 has no elements") + .insert_str(0, "and "); + input_types.join(", ") + } + }; + + match input { + PipelineData::Empty => Err(ShellError::PipelineEmpty { dst_span: head }), + _ => Err(ShellError::OnlySupportsThisInputType { + exp_input_type: expected_string, + wrong_type: input.get_type().to_string(), + dst_span: head, + src_span: input.span().unwrap_or(Span::unknown()), + }), + } +} + +/// Get variable from [`Stack`] or [`EngineState`] +fn get_var(ctx: &EvalContext<'_>, var_id: VarId, span: Span) -> Result { + match var_id { + // $env + ENV_VARIABLE_ID => { + let env_vars = ctx.stack.get_env_vars(ctx.engine_state); + let env_columns = env_vars.keys(); + let env_values = env_vars.values(); + + let mut pairs = env_columns + .map(|x| x.to_string()) + .zip(env_values.cloned()) + .collect::>(); + + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + + Ok(Value::record(pairs.into_iter().collect(), span)) + } + _ => ctx.stack.get_var(var_id, span).or_else(|err| { + // $nu is handled by getting constant + if let Some(const_val) = ctx.engine_state.get_constant(var_id).cloned() { + Ok(const_val.with_span(span)) + } else { + Err(err) + } + }), + } +} + +/// Get an environment variable, case-insensitively +fn get_env_var_case_insensitive<'a>(ctx: &'a mut EvalContext<'_>, key: &str) -> Option<&'a Value> { + // Read scopes in order + for overlays in ctx + .stack + .env_vars + .iter() + .rev() + .chain(std::iter::once(&ctx.engine_state.env_vars)) + { + // Read overlays in order + for overlay_name in ctx.stack.active_overlays.iter().rev() { + let Some(map) = overlays.get(overlay_name) else { + // Skip if overlay doesn't exist in this scope + continue; + }; + let hidden = ctx.stack.env_hidden.get(overlay_name); + let is_hidden = |key: &str| hidden.is_some_and(|hidden| hidden.contains(key)); + + if let Some(val) = map + // Check for exact match + .get(key) + // Skip when encountering an overlay where the key is hidden + .filter(|_| !is_hidden(key)) + .or_else(|| { + // Check to see if it exists at all in the map, with a different case + map.iter().find_map(|(k, v)| { + // Again, skip something that's hidden + (k.eq_ignore_case(key) && !is_hidden(k)).then_some(v) + }) + }) + { + return Some(val); + } + } + } + // Not found + None +} + +/// Get the existing name of an environment variable, case-insensitively. This is used to implement +/// case preservation of environment variables, so that changing an environment variable that +/// already exists always uses the same case. +fn get_env_var_name_case_insensitive<'a>(ctx: &mut EvalContext<'_>, key: &'a str) -> Cow<'a, str> { + // Read scopes in order + ctx.stack + .env_vars + .iter() + .rev() + .chain(std::iter::once(&ctx.engine_state.env_vars)) + .flat_map(|overlays| { + // Read overlays in order + ctx.stack + .active_overlays + .iter() + .rev() + .filter_map(|name| overlays.get(name)) + }) + .find_map(|map| { + // Use the hashmap first to try to be faster? + if map.contains_key(key) { + Some(Cow::Borrowed(key)) + } else { + map.keys().find(|k| k.eq_ignore_case(key)).map(|k| { + // it exists, but with a different case + Cow::Owned(k.to_owned()) + }) + } + }) + // didn't exist. + .unwrap_or(Cow::Borrowed(key)) +} + +/// Helper to collect values into [`PipelineData`], preserving original span and metadata +/// +/// The metadata is removed if it is the file data source, as that's just meant to mark streams. +fn collect(data: PipelineData, fallback_span: Span) -> Result { + let span = data.span().unwrap_or(fallback_span); + let metadata = match data.metadata() { + // Remove the `FilePath` metadata, because after `collect` it's no longer necessary to + // check where some input came from. + Some(PipelineMetadata { + data_source: DataSource::FilePath(_), + content_type: None, + }) => None, + other => other, + }; + let value = data.into_value(span)?; + Ok(PipelineData::Value(value, metadata)) +} + +/// Helper for drain behavior. +fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result { + use self::InstructionResult::*; + match data { + PipelineData::ByteStream(stream, ..) => { + let span = stream.span(); + let callback_spans = stream.get_caller_spans().clone(); + if let Err(mut err) = stream.drain() { + ctx.stack.set_last_error(&err); + if callback_spans.is_empty() { + return Err(err); + } else { + for s in callback_spans { + err = ShellError::EvalBlockWithInput { + span: s, + sources: vec![err], + } + } + return Err(err); + } + } else { + ctx.stack.set_last_exit_code(0, span); + } + } + PipelineData::ListStream(stream, ..) => { + let callback_spans = stream.get_caller_spans().clone(); + if let Err(mut err) = stream.drain() { + if callback_spans.is_empty() { + return Err(err); + } else { + for s in callback_spans { + err = ShellError::EvalBlockWithInput { + span: s, + sources: vec![err], + } + } + return Err(err); + } + } + } + PipelineData::Value(..) | PipelineData::Empty => {} + } + Ok(Continue) +} + +enum RedirectionStream { + Out, + Err, +} + +/// Open a file for redirection +fn open_file(ctx: &EvalContext<'_>, path: &Value, append: bool) -> Result, ShellError> { + let path_expanded = + expand_path_with(path.as_str()?, ctx.engine_state.cwd(Some(ctx.stack))?, true); + let mut options = File::options(); + if append { + options.append(true); + } else { + options.write(true).truncate(true); + } + let file = options + .create(true) + .open(&path_expanded) + .map_err(|err| IoError::new(err, path.span(), path_expanded))?; + Ok(Arc::new(file)) +} + +/// Set up a [`Redirection`] from a [`RedirectMode`] +fn eval_redirection( + ctx: &mut EvalContext<'_>, + mode: &RedirectMode, + span: Span, + which: RedirectionStream, +) -> Result, ShellError> { + match mode { + RedirectMode::Pipe => Ok(Some(Redirection::Pipe(OutDest::Pipe))), + RedirectMode::PipeSeparate => Ok(Some(Redirection::Pipe(OutDest::PipeSeparate))), + RedirectMode::Value => Ok(Some(Redirection::Pipe(OutDest::Value))), + RedirectMode::Null => Ok(Some(Redirection::Pipe(OutDest::Null))), + RedirectMode::Inherit => Ok(Some(Redirection::Pipe(OutDest::Inherit))), + RedirectMode::Print => Ok(Some(Redirection::Pipe(OutDest::Print))), + RedirectMode::File { file_num } => { + let file = ctx + .files + .get(*file_num as usize) + .cloned() + .flatten() + .ok_or_else(|| ShellError::IrEvalError { + msg: format!("Tried to redirect to file #{file_num}, but it is not open"), + span: Some(span), + })?; + Ok(Some(Redirection::File(file))) + } + RedirectMode::Caller => Ok(match which { + RedirectionStream::Out => ctx.stack.pipe_stdout().cloned().map(Redirection::Pipe), + RedirectionStream::Err => ctx.stack.pipe_stderr().cloned().map(Redirection::Pipe), + }), + } +} + +/// Do an `iterate` instruction. This can be called repeatedly to get more values from an iterable +fn eval_iterate( + ctx: &mut EvalContext<'_>, + dst: RegId, + stream: RegId, + end_index: usize, +) -> Result { + let mut data = ctx.take_reg(stream); + if let PipelineData::ListStream(list_stream, _) = &mut data { + // Modify the stream, taking one value off, and branching if it's empty + if let Some(val) = list_stream.next_value() { + ctx.put_reg(dst, val.into_pipeline_data()); + ctx.put_reg(stream, data); // put the stream back so it can be iterated on again + Ok(InstructionResult::Continue) + } else { + ctx.put_reg(dst, PipelineData::Empty); + Ok(InstructionResult::Branch(end_index)) + } + } else { + // Convert the PipelineData to an iterator, and wrap it in a ListStream so it can be + // iterated on + let metadata = data.metadata(); + let span = data.span().unwrap_or(Span::unknown()); + ctx.put_reg( + stream, + PipelineData::ListStream( + ListStream::new(data.into_iter(), span, Signals::EMPTY), + metadata, + ), + ); + eval_iterate(ctx, dst, stream, end_index) + } +} + +/// Redirect environment from the callee stack to the caller stack +fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee_stack: &Stack) { + // TODO: make this more efficient + // Grab all environment variables from the callee + let caller_env_vars = caller_stack.get_env_var_names(engine_state); + + // remove env vars that are present in the caller but not in the callee + // (the callee hid them) + for var in caller_env_vars.iter() { + if !callee_stack.has_env_var(engine_state, var) { + caller_stack.remove_env_var(engine_state, var); + } + } + + // add new env vars from callee to caller + for (var, value) in callee_stack.get_stack_env_vars() { + caller_stack.add_env_var(var, value); + } + + // set config to callee config, to capture any updates to that + caller_stack.config.clone_from(&callee_stack.config); +} diff --git a/nushell/crates/nu-engine/src/exit.rs b/nushell/crates/nu-engine/src/exit.rs new file mode 100644 index 0000000..396b3ef --- /dev/null +++ b/nushell/crates/nu-engine/src/exit.rs @@ -0,0 +1,37 @@ +use std::sync::atomic::Ordering; + +use nu_protocol::engine::EngineState; + +/// Exit the process or clean jobs if appropriate. +/// +/// Drops `tag` and exits the current process if there are no running jobs, or if `exit_warning_given` is true. +/// When running in an interactive session, warns the user if there +/// were jobs and sets `exit_warning_given` instead, returning `tag` itself in that case. +/// +// Currently, this `tag` argument exists mostly so that a LineEditor can be dropped before exiting the process. +pub fn cleanup_exit(tag: T, engine_state: &EngineState, exit_code: i32) -> T { + let mut jobs = engine_state.jobs.lock().expect("failed to lock job table"); + + if engine_state.is_interactive + && jobs.iter().next().is_some() + && !engine_state.exit_warning_given.load(Ordering::SeqCst) + { + let job_count = jobs.iter().count(); + + println!("There are still background jobs running ({}).", job_count); + + println!("Running `exit` a second time will kill all of them."); + + engine_state + .exit_warning_given + .store(true, Ordering::SeqCst); + + return tag; + } + + let _ = jobs.kill_all(); + + drop(tag); + + std::process::exit(exit_code); +} diff --git a/nushell/crates/nu-engine/src/glob_from.rs b/nushell/crates/nu-engine/src/glob_from.rs new file mode 100644 index 0000000..7753c63 --- /dev/null +++ b/nushell/crates/nu-engine/src/glob_from.rs @@ -0,0 +1,117 @@ +use nu_glob::MatchOptions; +use nu_path::{canonicalize_with, expand_path_with}; +use nu_protocol::{NuGlob, ShellError, Signals, Span, Spanned, shell_error::io::IoError}; +use std::{ + fs, + path::{Component, Path, PathBuf}, +}; + +/// This function is like `nu_glob::glob` from the `glob` crate, except it is relative to a given cwd. +/// +/// It returns a tuple of two values: the first is an optional prefix that the expanded filenames share. +/// This prefix can be removed from the front of each value to give an approximation of the relative path +/// to the user +/// +/// The second of the two values is an iterator over the matching filepaths. +#[allow(clippy::type_complexity)] +pub fn glob_from( + pattern: &Spanned, + cwd: &Path, + span: Span, + options: Option, + signals: Signals, +) -> Result< + ( + Option, + Box> + Send>, + ), + ShellError, +> { + let no_glob_for_pattern = matches!(pattern.item, NuGlob::DoNotExpand(_)); + let pattern_span = pattern.span; + let (prefix, pattern) = if nu_glob::is_glob(pattern.item.as_ref()) { + // Pattern contains glob, split it + let mut p = PathBuf::new(); + let path = PathBuf::from(&pattern.item.as_ref()); + let components = path.components(); + let mut counter = 0; + + for c in components { + if let Component::Normal(os) = c { + if nu_glob::is_glob(os.to_string_lossy().as_ref()) { + break; + } + } + p.push(c); + counter += 1; + } + + let mut just_pattern = PathBuf::new(); + for c in counter..path.components().count() { + if let Some(comp) = path.components().nth(c) { + just_pattern.push(comp); + } + } + if no_glob_for_pattern { + just_pattern = PathBuf::from(nu_glob::Pattern::escape(&just_pattern.to_string_lossy())); + } + + // Now expand `p` to get full prefix + let path = expand_path_with(p, cwd, pattern.item.is_expand()); + let escaped_prefix = PathBuf::from(nu_glob::Pattern::escape(&path.to_string_lossy())); + + (Some(path), escaped_prefix.join(just_pattern)) + } else { + let path = PathBuf::from(&pattern.item.as_ref()); + let path = expand_path_with(path, cwd, pattern.item.is_expand()); + let is_symlink = match fs::symlink_metadata(&path) { + Ok(attr) => attr.file_type().is_symlink(), + Err(_) => false, + }; + + if is_symlink { + (path.parent().map(|parent| parent.to_path_buf()), path) + } else { + let path = match canonicalize_with(path.clone(), cwd) { + Ok(p) if nu_glob::is_glob(p.to_string_lossy().as_ref()) => { + // our path might contain glob metacharacters too. + // in such case, we need to escape our path to make + // glob work successfully + PathBuf::from(nu_glob::Pattern::escape(&p.to_string_lossy())) + } + Ok(p) => p, + Err(err) => { + return Err(IoError::new(err, pattern_span, path).into()); + } + }; + (path.parent().map(|parent| parent.to_path_buf()), path) + } + }; + + let pattern = pattern.to_string_lossy().to_string(); + let glob_options = options.unwrap_or_default(); + + let glob = nu_glob::glob_with(&pattern, glob_options, signals).map_err(|e| { + nu_protocol::ShellError::GenericError { + error: "Error extracting glob pattern".into(), + msg: e.to_string(), + span: Some(span), + help: None, + inner: vec![], + } + })?; + + Ok(( + prefix, + Box::new(glob.map(move |x| match x { + Ok(v) => Ok(v), + Err(e) => Err(nu_protocol::ShellError::GenericError { + error: "Error extracting glob pattern".into(), + msg: e.to_string(), + span: Some(span), + help: None, + inner: vec![], + }), + })), + )) +} diff --git a/nushell/crates/nu-engine/src/lib.rs b/nushell/crates/nu-engine/src/lib.rs new file mode 100644 index 0000000..0c920b8 --- /dev/null +++ b/nushell/crates/nu-engine/src/lib.rs @@ -0,0 +1,28 @@ +#![doc = include_str!("../README.md")] +mod call_ext; +mod closure_eval; +pub mod column; +pub mod command_prelude; +mod compile; +pub mod documentation; +pub mod env; +mod eval; +mod eval_helpers; +mod eval_ir; +pub mod exit; +mod glob_from; +pub mod scope; + +pub use call_ext::CallExt; +pub use closure_eval::*; +pub use column::get_columns; +pub use compile::compile; +pub use documentation::get_full_help; +pub use env::*; +pub use eval::{ + eval_block, eval_block_with_early_return, eval_call, eval_expression, + eval_expression_with_input, eval_subexpression, eval_variable, redirect_env, +}; +pub use eval_helpers::*; +pub use eval_ir::eval_ir_block; +pub use glob_from::glob_from; diff --git a/nushell/crates/nu-engine/src/scope.rs b/nushell/crates/nu-engine/src/scope.rs new file mode 100644 index 0000000..576a1a4 --- /dev/null +++ b/nushell/crates/nu-engine/src/scope.rs @@ -0,0 +1,575 @@ +use nu_protocol::{ + DeclId, ModuleId, Signature, Span, SyntaxShape, Type, Value, VarId, + ast::Expr, + engine::{Command, EngineState, Stack, Visibility}, + record, +}; +use std::{cmp::Ordering, collections::HashMap}; + +pub struct ScopeData<'e, 's> { + engine_state: &'e EngineState, + stack: &'s Stack, + vars_map: HashMap<&'e Vec, &'e VarId>, + decls_map: HashMap<&'e Vec, &'e DeclId>, + modules_map: HashMap<&'e Vec, &'e ModuleId>, + visibility: Visibility, +} + +impl<'e, 's> ScopeData<'e, 's> { + pub fn new(engine_state: &'e EngineState, stack: &'s Stack) -> Self { + Self { + engine_state, + stack, + vars_map: HashMap::new(), + decls_map: HashMap::new(), + modules_map: HashMap::new(), + visibility: Visibility::new(), + } + } + + pub fn populate_vars(&mut self) { + for overlay_frame in self.engine_state.active_overlays(&[]) { + self.vars_map.extend(&overlay_frame.vars); + } + } + + // decls include all commands, i.e., normal commands, aliases, and externals + pub fn populate_decls(&mut self) { + for overlay_frame in self.engine_state.active_overlays(&[]) { + self.decls_map.extend(&overlay_frame.decls); + self.visibility.merge_with(overlay_frame.visibility.clone()); + } + } + + pub fn populate_modules(&mut self) { + for overlay_frame in self.engine_state.active_overlays(&[]) { + self.modules_map.extend(&overlay_frame.modules); + } + } + + pub fn collect_vars(&self, span: Span) -> Vec { + let mut vars = vec![]; + + for (var_name, var_id) in &self.vars_map { + let var_name = Value::string(String::from_utf8_lossy(var_name).to_string(), span); + + let var = self.engine_state.get_var(**var_id); + let var_type = Value::string(var.ty.to_string(), span); + let is_const = Value::bool(var.const_val.is_some(), span); + + let var_value = self + .stack + .get_var(**var_id, span) + .ok() + .or(var.const_val.clone()) + .unwrap_or(Value::nothing(span)); + + let var_id_val = Value::int(var_id.get() as i64, span); + + vars.push(Value::record( + record! { + "name" => var_name, + "type" => var_type, + "value" => var_value, + "is_const" => is_const, + "var_id" => var_id_val, + }, + span, + )); + } + + sort_rows(&mut vars); + vars + } + + pub fn collect_commands(&self, span: Span) -> Vec { + let mut commands = vec![]; + + for (command_name, decl_id) in &self.decls_map { + if self.visibility.is_decl_id_visible(decl_id) + && !self.engine_state.get_decl(**decl_id).is_alias() + { + let decl = self.engine_state.get_decl(**decl_id); + let signature = decl.signature(); + + let examples = decl + .examples() + .into_iter() + .map(|x| { + Value::record( + record! { + "description" => Value::string(x.description, span), + "example" => Value::string(x.example, span), + "result" => x.result.unwrap_or(Value::nothing(span)), + }, + span, + ) + }) + .collect(); + + let attributes = decl + .attributes() + .into_iter() + .map(|(name, value)| { + Value::record( + record! { + "name" => Value::string(name, span), + "value" => value, + }, + span, + ) + }) + .collect(); + + let record = record! { + "name" => Value::string(String::from_utf8_lossy(command_name), span), + "category" => Value::string(signature.category.to_string(), span), + "signatures" => self.collect_signatures(&signature, span), + "description" => Value::string(decl.description(), span), + "examples" => Value::list(examples, span), + "attributes" => Value::list(attributes, span), + "type" => Value::string(decl.command_type().to_string(), span), + "is_sub" => Value::bool(decl.is_sub(), span), + "is_const" => Value::bool(decl.is_const(), span), + "creates_scope" => Value::bool(signature.creates_scope, span), + "extra_description" => Value::string(decl.extra_description(), span), + "search_terms" => Value::string(decl.search_terms().join(", "), span), + "decl_id" => Value::int(decl_id.get() as i64, span), + }; + + commands.push(Value::record(record, span)) + } + } + + sort_rows(&mut commands); + + commands + } + + fn collect_signatures(&self, signature: &Signature, span: Span) -> Value { + let mut sigs = signature + .input_output_types + .iter() + .map(|(input_type, output_type)| { + ( + input_type.to_shape().to_string(), + Value::list( + self.collect_signature_entries(input_type, output_type, signature, span), + span, + ), + ) + }) + .collect::>(); + + // Until we allow custom commands to have input and output types, let's just + // make them Type::Any Type::Any so they can show up in our `scope commands` + // a little bit better. If sigs is empty, we're pretty sure that we're dealing + // with a custom command. + if sigs.is_empty() { + let any_type = &Type::Any; + sigs.push(( + any_type.to_shape().to_string(), + Value::list( + self.collect_signature_entries(any_type, any_type, signature, span), + span, + ), + )); + } + sigs.sort_unstable_by(|(k1, _), (k2, _)| k1.cmp(k2)); + // For most commands, input types are not repeated in + // `input_output_types`, i.e. each input type has only one associated + // output type. Furthermore, we want this to always be true. However, + // there are currently some exceptions, such as `hash sha256` which + // takes in string but may output string or binary depending on the + // presence of the --binary flag. In such cases, the "special case" + // signature usually comes later in the input_output_types, so this will + // remove them from the record. + sigs.dedup_by(|(k1, _), (k2, _)| k1 == k2); + Value::record(sigs.into_iter().collect(), span) + } + + fn collect_signature_entries( + &self, + input_type: &Type, + output_type: &Type, + signature: &Signature, + span: Span, + ) -> Vec { + let mut sig_records = vec![]; + + // input + sig_records.push(Value::record( + record! { + "parameter_name" => Value::nothing(span), + "parameter_type" => Value::string("input", span), + "syntax_shape" => Value::string(input_type.to_shape().to_string(), span), + "is_optional" => Value::bool(false, span), + "short_flag" => Value::nothing(span), + "description" => Value::nothing(span), + "custom_completion" => Value::nothing(span), + "parameter_default" => Value::nothing(span), + }, + span, + )); + + // required_positional + for req in &signature.required_positional { + let custom = extract_custom_completion_from_arg(self.engine_state, &req.shape); + + sig_records.push(Value::record( + record! { + "parameter_name" => Value::string(&req.name, span), + "parameter_type" => Value::string("positional", span), + "syntax_shape" => Value::string(req.shape.to_string(), span), + "is_optional" => Value::bool(false, span), + "short_flag" => Value::nothing(span), + "description" => Value::string(&req.desc, span), + "custom_completion" => Value::string(custom, span), + "parameter_default" => Value::nothing(span), + }, + span, + )); + } + + // optional_positional + for opt in &signature.optional_positional { + let custom = extract_custom_completion_from_arg(self.engine_state, &opt.shape); + let default = if let Some(val) = &opt.default_value { + val.clone() + } else { + Value::nothing(span) + }; + + sig_records.push(Value::record( + record! { + "parameter_name" => Value::string(&opt.name, span), + "parameter_type" => Value::string("positional", span), + "syntax_shape" => Value::string(opt.shape.to_string(), span), + "is_optional" => Value::bool(true, span), + "short_flag" => Value::nothing(span), + "description" => Value::string(&opt.desc, span), + "custom_completion" => Value::string(custom, span), + "parameter_default" => default, + }, + span, + )); + } + + // rest_positional + if let Some(rest) = &signature.rest_positional { + let name = if rest.name == "rest" { "" } else { &rest.name }; + let custom = extract_custom_completion_from_arg(self.engine_state, &rest.shape); + + sig_records.push(Value::record( + record! { + "parameter_name" => Value::string(name, span), + "parameter_type" => Value::string("rest", span), + "syntax_shape" => Value::string(rest.shape.to_string(), span), + "is_optional" => Value::bool(true, span), + "short_flag" => Value::nothing(span), + "description" => Value::string(&rest.desc, span), + "custom_completion" => Value::string(custom, span), + // rest_positional does have default, but parser prohibits specifying it?! + "parameter_default" => Value::nothing(span), + }, + span, + )); + } + + // named flags + for named in &signature.named { + let flag_type; + + // Skip the help flag + if named.long == "help" { + continue; + } + + let mut custom_completion_command_name: String = "".to_string(); + let shape = if let Some(arg) = &named.arg { + flag_type = Value::string("named", span); + custom_completion_command_name = + extract_custom_completion_from_arg(self.engine_state, arg); + Value::string(arg.to_string(), span) + } else { + flag_type = Value::string("switch", span); + Value::nothing(span) + }; + + let short_flag = if let Some(c) = named.short { + Value::string(c, span) + } else { + Value::nothing(span) + }; + + let default = if let Some(val) = &named.default_value { + val.clone() + } else { + Value::nothing(span) + }; + + sig_records.push(Value::record( + record! { + "parameter_name" => Value::string(&named.long, span), + "parameter_type" => flag_type, + "syntax_shape" => shape, + "is_optional" => Value::bool(!named.required, span), + "short_flag" => short_flag, + "description" => Value::string(&named.desc, span), + "custom_completion" => Value::string(custom_completion_command_name, span), + "parameter_default" => default, + }, + span, + )); + } + + // output + sig_records.push(Value::record( + record! { + "parameter_name" => Value::nothing(span), + "parameter_type" => Value::string("output", span), + "syntax_shape" => Value::string(output_type.to_shape().to_string(), span), + "is_optional" => Value::bool(false, span), + "short_flag" => Value::nothing(span), + "description" => Value::nothing(span), + "custom_completion" => Value::nothing(span), + "parameter_default" => Value::nothing(span), + }, + span, + )); + + sig_records + } + + pub fn collect_externs(&self, span: Span) -> Vec { + let mut externals = vec![]; + + for (command_name, decl_id) in &self.decls_map { + let decl = self.engine_state.get_decl(**decl_id); + + if decl.is_known_external() { + let record = record! { + "name" => Value::string(String::from_utf8_lossy(command_name), span), + "description" => Value::string(decl.description(), span), + "decl_id" => Value::int(decl_id.get() as i64, span), + }; + + externals.push(Value::record(record, span)) + } + } + + sort_rows(&mut externals); + externals + } + + pub fn collect_aliases(&self, span: Span) -> Vec { + let mut aliases = vec![]; + + for (decl_name, decl_id) in self.engine_state.get_decls_sorted(false) { + if self.visibility.is_decl_id_visible(&decl_id) { + let decl = self.engine_state.get_decl(decl_id); + if let Some(alias) = decl.as_alias() { + let aliased_decl_id = if let Expr::Call(wrapped_call) = &alias.wrapped_call.expr + { + Value::int(wrapped_call.decl_id.get() as i64, span) + } else { + Value::nothing(span) + }; + + let expansion = String::from_utf8_lossy( + self.engine_state.get_span_contents(alias.wrapped_call.span), + ); + + aliases.push(Value::record( + record! { + "name" => Value::string(String::from_utf8_lossy(&decl_name), span), + "expansion" => Value::string(expansion, span), + "description" => Value::string(alias.description(), span), + "decl_id" => Value::int(decl_id.get() as i64, span), + "aliased_decl_id" => aliased_decl_id, + }, + span, + )); + } + } + } + + sort_rows(&mut aliases); + // aliases.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + aliases + } + + fn collect_module(&self, module_name: &[u8], module_id: &ModuleId, span: Span) -> Value { + let module = self.engine_state.get_module(*module_id); + + let all_decls = module.decls(); + + let mut export_commands: Vec = all_decls + .iter() + .filter_map(|(name_bytes, decl_id)| { + let decl = self.engine_state.get_decl(*decl_id); + + if !decl.is_alias() && !decl.is_known_external() { + Some(Value::record( + record! { + "name" => Value::string(String::from_utf8_lossy(name_bytes), span), + "decl_id" => Value::int(decl_id.get() as i64, span), + }, + span, + )) + } else { + None + } + }) + .collect(); + + let mut export_aliases: Vec = all_decls + .iter() + .filter_map(|(name_bytes, decl_id)| { + let decl = self.engine_state.get_decl(*decl_id); + + if decl.is_alias() { + Some(Value::record( + record! { + "name" => Value::string(String::from_utf8_lossy(name_bytes), span), + "decl_id" => Value::int(decl_id.get() as i64, span), + }, + span, + )) + } else { + None + } + }) + .collect(); + + let mut export_externs: Vec = all_decls + .iter() + .filter_map(|(name_bytes, decl_id)| { + let decl = self.engine_state.get_decl(*decl_id); + + if decl.is_known_external() { + Some(Value::record( + record! { + "name" => Value::string(String::from_utf8_lossy(name_bytes), span), + "decl_id" => Value::int(decl_id.get() as i64, span), + }, + span, + )) + } else { + None + } + }) + .collect(); + + let mut export_submodules: Vec = module + .submodules() + .iter() + .map(|(name_bytes, submodule_id)| self.collect_module(name_bytes, submodule_id, span)) + .collect(); + + let mut export_consts: Vec = module + .consts() + .iter() + .map(|(name_bytes, var_id)| { + Value::record( + record! { + "name" => Value::string(String::from_utf8_lossy(name_bytes), span), + "type" => Value::string(self.engine_state.get_var(*var_id).ty.to_string(), span), + "var_id" => Value::int(var_id.get() as i64, span), + }, + span, + ) + }) + .collect(); + + sort_rows(&mut export_commands); + sort_rows(&mut export_aliases); + sort_rows(&mut export_externs); + sort_rows(&mut export_submodules); + sort_rows(&mut export_consts); + + let (module_desc, module_extra_desc) = self + .engine_state + .build_module_desc(*module_id) + .unwrap_or_default(); + + Value::record( + record! { + "name" => Value::string(String::from_utf8_lossy(module_name), span), + "commands" => Value::list(export_commands, span), + "aliases" => Value::list(export_aliases, span), + "externs" => Value::list(export_externs, span), + "submodules" => Value::list(export_submodules, span), + "constants" => Value::list(export_consts, span), + "has_env_block" => Value::bool(module.env_block.is_some(), span), + "description" => Value::string(module_desc, span), + "extra_description" => Value::string(module_extra_desc, span), + "module_id" => Value::int(module_id.get() as i64, span), + "file" => Value::string(module.file.clone().map_or("unknown".to_string(), |(p, _)| p.path().to_string_lossy().to_string()), span), + }, + span, + ) + } + + pub fn collect_modules(&self, span: Span) -> Vec { + let mut modules = vec![]; + + for (module_name, module_id) in &self.modules_map { + modules.push(self.collect_module(module_name, module_id, span)); + } + + modules.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + modules + } + + pub fn collect_engine_state(&self, span: Span) -> Value { + let num_env_vars = self + .engine_state + .env_vars + .values() + .map(|overlay| overlay.len() as i64) + .sum(); + + Value::record( + record! { + "source_bytes" => Value::int(self.engine_state.next_span_start() as i64, span), + "num_vars" => Value::int(self.engine_state.num_vars() as i64, span), + "num_decls" => Value::int(self.engine_state.num_decls() as i64, span), + "num_blocks" => Value::int(self.engine_state.num_blocks() as i64, span), + "num_modules" => Value::int(self.engine_state.num_modules() as i64, span), + "num_env_vars" => Value::int(num_env_vars, span), + }, + span, + ) + } +} + +fn extract_custom_completion_from_arg(engine_state: &EngineState, shape: &SyntaxShape) -> String { + match shape { + SyntaxShape::CompleterWrapper(_, custom_completion_decl_id) => { + let custom_completion_command = engine_state.get_decl(*custom_completion_decl_id); + let custom_completion_command_name: &str = custom_completion_command.name(); + custom_completion_command_name.to_string() + } + _ => "".to_string(), + } +} + +fn sort_rows(decls: &mut [Value]) { + decls.sort_by(|a, b| match (a, b) { + (Value::Record { val: rec_a, .. }, Value::Record { val: rec_b, .. }) => { + // Comparing the first value from the record + // It is expected that the first value is the name of the entry (command, module, alias, etc.) + match (rec_a.values().next(), rec_b.values().next()) { + (Some(val_a), Some(val_b)) => match (val_a, val_b) { + (Value::String { val: str_a, .. }, Value::String { val: str_b, .. }) => { + str_a.cmp(str_b) + } + _ => Ordering::Equal, + }, + _ => Ordering::Equal, + } + } + _ => Ordering::Equal, + }); +} diff --git a/nushell/crates/nu-explore/.gitignore b/nushell/crates/nu-explore/.gitignore new file mode 100644 index 0000000..4c234e5 --- /dev/null +++ b/nushell/crates/nu-explore/.gitignore @@ -0,0 +1,22 @@ +/target +/scratch +**/*.rs.bk +history.txt +tests/fixtures/nuplayground +crates/*/target + +# Debian/Ubuntu +debian/.debhelper/ +debian/debhelper-build-stamp +debian/files +debian/nu.substvars +debian/nu/ + +# macOS junk +.DS_Store + +# JetBrains' IDE items +.idea/* + +# VSCode's IDE items +.vscode/* diff --git a/nushell/crates/nu-explore/Cargo.toml b/nushell/crates/nu-explore/Cargo.toml new file mode 100644 index 0000000..0f876d7 --- /dev/null +++ b/nushell/crates/nu-explore/Cargo.toml @@ -0,0 +1,37 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Nushell table pager" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-explore" +edition = "2024" +license = "MIT" +name = "nu-explore" +version = "0.105.2" + +[lib] +bench = false + +[lints] +workspace = true + +[dependencies] +nu-protocol = { path = "../nu-protocol", version = "0.105.2" } +nu-parser = { path = "../nu-parser", version = "0.105.2" } +nu-path = { path = "../nu-path", version = "0.105.2" } +nu-color-config = { path = "../nu-color-config", version = "0.105.2" } +nu-engine = { path = "../nu-engine", version = "0.105.2" } +nu-table = { path = "../nu-table", version = "0.105.2" } +nu-json = { path = "../nu-json", version = "0.105.2" } +nu-utils = { path = "../nu-utils", version = "0.105.2" } +nu-ansi-term = { workspace = true } +nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.105.2" } + +anyhow = { workspace = true } +log = { workspace = true } +strip-ansi-escapes = { workspace = true } +crossterm = { workspace = true } +ratatui = { workspace = true } +ansi-str = { workspace = true } +unicode-width = { workspace = true } +lscolors = { workspace = true, default-features = false, features = [ + "nu-ansi-term", +] } diff --git a/nushell/crates/nu-explore/LICENSE b/nushell/crates/nu-explore/LICENSE new file mode 100644 index 0000000..ae174e8 --- /dev/null +++ b/nushell/crates/nu-explore/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nushell/crates/nu-explore/README.md b/nushell/crates/nu-explore/README.md new file mode 100644 index 0000000..d18a576 --- /dev/null +++ b/nushell/crates/nu-explore/README.md @@ -0,0 +1,5 @@ +Implementation of the interactive `explore` command pager. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/nushell/crates/nu-explore/src/commands/expand.rs b/nushell/crates/nu-explore/src/commands/expand.rs new file mode 100644 index 0000000..8c6ad06 --- /dev/null +++ b/nushell/crates/nu-explore/src/commands/expand.rs @@ -0,0 +1,77 @@ +use super::ViewCommand; +use crate::{ + nu_common::{self, collect_input}, + views::{Preview, ViewConfig}, +}; +use anyhow::Result; +use nu_color_config::StyleComputer; +use nu_protocol::{ + Value, + engine::{EngineState, Stack}, +}; + +#[derive(Default, Clone)] +pub struct ExpandCmd; + +impl ExpandCmd { + pub fn new() -> Self { + Self + } +} + +impl ExpandCmd { + pub const NAME: &'static str = "expand"; +} + +impl ViewCommand for ExpandCmd { + type View = Preview; + + fn name(&self) -> &'static str { + Self::NAME + } + + fn description(&self) -> &'static str { + "" + } + + fn parse(&mut self, _: &str) -> Result<()> { + Ok(()) + } + + fn spawn( + &mut self, + engine_state: &EngineState, + stack: &mut Stack, + value: Option, + _: &ViewConfig, + ) -> Result { + if let Some(value) = value { + let value_as_string = convert_value_to_string(value, engine_state, stack)?; + Ok(Preview::new(&value_as_string)) + } else { + Ok(Preview::new("")) + } + } +} + +fn convert_value_to_string( + value: Value, + engine_state: &EngineState, + stack: &mut Stack, +) -> Result { + let (cols, vals) = collect_input(value.clone())?; + + let has_no_head = cols.is_empty() || (cols.len() == 1 && cols[0].is_empty()); + let has_single_value = vals.len() == 1 && vals[0].len() == 1; + if !has_no_head && has_single_value { + let config = stack.get_config(engine_state); + Ok(vals[0][0].to_abbreviated_string(&config)) + } else { + let config = engine_state.get_config(); + let style_computer = StyleComputer::from_config(engine_state, stack); + let table = + nu_common::try_build_table(value, engine_state.signals(), config, style_computer); + + Ok(table) + } +} diff --git a/nushell/crates/nu-explore/src/commands/help.rs b/nushell/crates/nu-explore/src/commands/help.rs new file mode 100644 index 0000000..47cba18 --- /dev/null +++ b/nushell/crates/nu-explore/src/commands/help.rs @@ -0,0 +1,113 @@ +use super::ViewCommand; +use crate::views::{Preview, ViewConfig}; +use anyhow::Result; +use nu_ansi_term::Color; +use nu_protocol::{ + Value, + engine::{EngineState, Stack}, +}; + +use std::sync::LazyLock; + +#[derive(Debug, Default, Clone)] +pub struct HelpCmd {} + +impl HelpCmd { + pub const NAME: &'static str = "help"; + pub fn view() -> Preview { + Preview::new(&HELP_MESSAGE) + } +} + +static HELP_MESSAGE: LazyLock = LazyLock::new(|| { + let title = nu_ansi_term::Style::new().bold().underline(); + let code = nu_ansi_term::Style::new().bold().fg(Color::Blue); + + // There is probably a nicer way to do this formatting inline + format!( + r#"{} +Explore helps you dynamically navigate through your data! + +{} +Launch Explore by piping data into it: {} + + Move around: Use the cursor keys +Drill down into records+tables: Press to select a cell, move around with cursor keys, press again + Go back/up a level: Press or "q" + Transpose (flip rows+columns): Press "t" + Expand (show all nested data): Press "e" + Open this help page : Type ":help" then + Open an interactive REPL: Type ":try" then + Run a Nushell command: Type ":nu " then . The data currently being explored is piped into it. + Scroll up: Press "Page Up", Ctrl+B, or Alt+V + Scroll down: Press "Page Down", Ctrl+F, or Ctrl+V + Exit Explore: Type ":q" then , or Ctrl+D. Alternately, press or "q" until Explore exits + +{} +Most commands support search via regular expressions. + +You can type "/" and type a pattern you want to search on. Then hit and you will see the search results. + +To go to the next hit use "" key. You also can do a reverse search by using "?" instead of "/". +"#, + title.paint("Explore"), + title.paint("Basics"), + code.paint("ls | explore"), + title.paint("Search") + ) +}); + +// TODO: search help could use some updating... search results get shown immediately after typing, don't need to press Enter +// const HELP_MESSAGE: &str = r#"# Explore + +// Explore helps you dynamically navigate through your data + +// ## Basics + +// Move around: Use the cursor keys +// Drill down into records+tables: Press to select a cell, move around with cursor keys, then press again +// Go back/up a level: Press +// Transpose data (flip rows and columns): Press "t" +// Expand data (show all nested data): Press "e" +// Open this help page : Type ":help" then +// Open an interactive REPL: Type ":try" then +// Scroll up/down: Use the "Page Up" and "Page Down" keys +// Exit Explore: Type ":q" then , or Ctrl+D. Alternately, press until Explore exits + +// ## Search + +// Most commands support search via regular expressions. + +// You can type "/" and type a pattern you want to search on. +// Then hit and you will see the search results. + +// To go to the next hit use "" key. + +// You also can do a reverse search by using "?" instead of "/". +// "#; + +impl ViewCommand for HelpCmd { + type View = Preview; + + fn name(&self) -> &'static str { + Self::NAME + } + + fn description(&self) -> &'static str { + "" + } + + fn parse(&mut self, _: &str) -> Result<()> { + Ok(()) + } + + fn spawn( + &mut self, + _: &EngineState, + _: &mut Stack, + _: Option, + _: &ViewConfig, + ) -> Result { + Ok(HelpCmd::view()) + } +} diff --git a/nushell/crates/nu-explore/src/commands/mod.rs b/nushell/crates/nu-explore/src/commands/mod.rs new file mode 100644 index 0000000..3010949 --- /dev/null +++ b/nushell/crates/nu-explore/src/commands/mod.rs @@ -0,0 +1,56 @@ +use crate::views::ViewConfig; + +use super::pager::{Pager, Transition}; +use anyhow::Result; +use nu_protocol::{ + Value, + engine::{EngineState, Stack}, +}; + +mod expand; +mod help; +mod nu; +mod quit; +mod table; +mod r#try; + +pub use expand::ExpandCmd; +pub use help::HelpCmd; +pub use nu::NuCmd; +pub use quit::QuitCmd; +pub use table::TableCmd; +pub use r#try::TryCmd; + +pub trait SimpleCommand { + fn name(&self) -> &'static str; + + fn description(&self) -> &'static str; + + fn parse(&mut self, args: &str) -> Result<()>; + + fn react( + &mut self, + engine_state: &EngineState, + stack: &mut Stack, + pager: &mut Pager<'_>, + value: Option, + ) -> Result; +} + +pub trait ViewCommand { + type View; + + fn name(&self) -> &'static str; + + fn description(&self) -> &'static str; + + fn parse(&mut self, args: &str) -> Result<()>; + + fn spawn( + &mut self, + engine_state: &EngineState, + stack: &mut Stack, + value: Option, + config: &ViewConfig, + ) -> Result; +} diff --git a/nushell/crates/nu-explore/src/commands/nu.rs b/nushell/crates/nu-explore/src/commands/nu.rs new file mode 100644 index 0000000..c788ca9 --- /dev/null +++ b/nushell/crates/nu-explore/src/commands/nu.rs @@ -0,0 +1,123 @@ +use super::ViewCommand; +use crate::{ + nu_common::{collect_pipeline, has_simple_value, run_command_with_value}, + pager::Frame, + views::{Layout, Orientation, Preview, RecordView, View, ViewConfig}, +}; +use anyhow::Result; +use nu_protocol::{ + PipelineData, Value, + engine::{EngineState, Stack}, +}; +use ratatui::layout::Rect; + +#[derive(Debug, Default, Clone)] +pub struct NuCmd { + command: String, +} + +impl NuCmd { + pub fn new() -> Self { + Self { + command: String::new(), + } + } + + pub const NAME: &'static str = "nu"; +} + +impl ViewCommand for NuCmd { + type View = NuView; + + fn name(&self) -> &'static str { + Self::NAME + } + + fn description(&self) -> &'static str { + "" + } + + fn parse(&mut self, args: &str) -> Result<()> { + args.trim().clone_into(&mut self.command); + + Ok(()) + } + + fn spawn( + &mut self, + engine_state: &EngineState, + stack: &mut Stack, + value: Option, + config: &ViewConfig, + ) -> Result { + let value = value.unwrap_or_default(); + + let pipeline = run_command_with_value(&self.command, &value, engine_state, stack)?; + + let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..)); + + let (columns, values) = collect_pipeline(pipeline)?; + + if let Some(value) = has_simple_value(&values) { + let text = value.to_abbreviated_string(&engine_state.config); + return Ok(NuView::Preview(Preview::new(&text))); + } + + let mut view = RecordView::new(columns, values, config.explore_config.clone()); + + if is_record { + view.set_top_layer_orientation(Orientation::Left); + } + + Ok(NuView::Records(Box::new(view))) + } +} + +pub enum NuView { + Records(Box), + Preview(Preview), +} + +impl View for NuView { + fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) { + match self { + NuView::Records(v) => v.draw(f, area, cfg, layout), + NuView::Preview(v) => v.draw(f, area, cfg, layout), + } + } + + fn handle_input( + &mut self, + engine_state: &EngineState, + stack: &mut Stack, + layout: &Layout, + info: &mut crate::pager::ViewInfo, + key: crossterm::event::KeyEvent, + ) -> crate::pager::Transition { + match self { + NuView::Records(v) => v.handle_input(engine_state, stack, layout, info, key), + NuView::Preview(v) => v.handle_input(engine_state, stack, layout, info, key), + } + } + + fn show_data(&mut self, i: usize) -> bool { + match self { + NuView::Records(v) => v.show_data(i), + NuView::Preview(v) => v.show_data(i), + } + } + + fn collect_data(&self) -> Vec { + match self { + NuView::Records(v) => v.collect_data(), + NuView::Preview(v) => v.collect_data(), + } + } + + fn exit(&mut self) -> Option { + match self { + NuView::Records(v) => v.exit(), + NuView::Preview(v) => v.exit(), + } + } +} diff --git a/nushell/crates/nu-explore/src/commands/quit.rs b/nushell/crates/nu-explore/src/commands/quit.rs new file mode 100644 index 0000000..fa0e9ca --- /dev/null +++ b/nushell/crates/nu-explore/src/commands/quit.rs @@ -0,0 +1,38 @@ +use super::SimpleCommand; +use crate::pager::{Pager, Transition}; +use anyhow::Result; +use nu_protocol::{ + Value, + engine::{EngineState, Stack}, +}; + +#[derive(Default, Clone)] +pub struct QuitCmd; + +impl QuitCmd { + pub const NAME: &'static str = "quit"; +} + +impl SimpleCommand for QuitCmd { + fn name(&self) -> &'static str { + Self::NAME + } + + fn description(&self) -> &'static str { + "" + } + + fn parse(&mut self, _: &str) -> Result<()> { + Ok(()) + } + + fn react( + &mut self, + _: &EngineState, + _: &mut Stack, + _: &mut Pager<'_>, + _: Option, + ) -> Result { + Ok(Transition::Exit) + } +} diff --git a/nushell/crates/nu-explore/src/commands/table.rs b/nushell/crates/nu-explore/src/commands/table.rs new file mode 100644 index 0000000..e035b76 --- /dev/null +++ b/nushell/crates/nu-explore/src/commands/table.rs @@ -0,0 +1,75 @@ +use super::ViewCommand; +use crate::{ + nu_common::collect_input, + views::{Orientation, RecordView, ViewConfig}, +}; +use anyhow::Result; +use nu_protocol::{ + Value, + engine::{EngineState, Stack}, +}; + +#[derive(Debug, Default, Clone)] +pub struct TableCmd { + // todo: add arguments to override config right from CMD + settings: TableSettings, +} + +#[derive(Debug, Default, Clone)] +struct TableSettings { + orientation: Option, + turn_on_cursor_mode: bool, +} + +impl TableCmd { + pub fn new() -> Self { + Self::default() + } + + pub const NAME: &'static str = "table"; +} + +impl ViewCommand for TableCmd { + type View = RecordView; + + fn name(&self) -> &'static str { + Self::NAME + } + + fn description(&self) -> &'static str { + "" + } + + fn parse(&mut self, _: &str) -> Result<()> { + Ok(()) + } + + fn spawn( + &mut self, + _: &EngineState, + _: &mut Stack, + value: Option, + config: &ViewConfig, + ) -> Result { + let value = value.unwrap_or_default(); + let is_record = matches!(value, Value::Record { .. }); + + let (columns, data) = collect_input(value)?; + + let mut view = RecordView::new(columns, data, config.explore_config.clone()); + + if is_record { + view.set_top_layer_orientation(Orientation::Left); + } + + if let Some(o) = self.settings.orientation { + view.set_top_layer_orientation(o); + } + + if self.settings.turn_on_cursor_mode { + view.set_cursor_mode(); + } + + Ok(view) + } +} diff --git a/nushell/crates/nu-explore/src/commands/try.rs b/nushell/crates/nu-explore/src/commands/try.rs new file mode 100644 index 0000000..926438b --- /dev/null +++ b/nushell/crates/nu-explore/src/commands/try.rs @@ -0,0 +1,55 @@ +use super::ViewCommand; +use crate::views::{TryView, ViewConfig}; +use anyhow::Result; +use nu_protocol::{ + Value, + engine::{EngineState, Stack}, +}; + +#[derive(Debug, Default, Clone)] +pub struct TryCmd { + command: String, +} + +impl TryCmd { + pub fn new() -> Self { + Self { + command: String::new(), + } + } + + pub const NAME: &'static str = "try"; +} + +impl ViewCommand for TryCmd { + type View = TryView; + + fn name(&self) -> &'static str { + Self::NAME + } + + fn description(&self) -> &'static str { + "" + } + + fn parse(&mut self, args: &str) -> Result<()> { + args.trim().clone_into(&mut self.command); + + Ok(()) + } + + fn spawn( + &mut self, + engine_state: &EngineState, + stack: &mut Stack, + value: Option, + config: &ViewConfig, + ) -> Result { + let value = value.unwrap_or_default(); + let mut view = TryView::new(value, config.explore_config.clone()); + view.init(self.command.clone()); + view.try_run(engine_state, stack)?; + + Ok(view) + } +} diff --git a/nushell/crates/nu-explore/src/default_context.rs b/nushell/crates/nu-explore/src/default_context.rs new file mode 100644 index 0000000..4d80375 --- /dev/null +++ b/nushell/crates/nu-explore/src/default_context.rs @@ -0,0 +1,17 @@ +use nu_protocol::engine::{EngineState, StateWorkingSet}; + +use crate::explore::*; + +pub fn add_explore_context(mut engine_state: EngineState) -> EngineState { + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + working_set.add_decl(Box::new(Explore)); + working_set.render() + }; + + if let Err(err) = engine_state.merge_delta(delta) { + eprintln!("Error creating explore command context: {err:?}"); + } + + engine_state +} diff --git a/nushell/crates/nu-explore/src/explore.rs b/nushell/crates/nu-explore/src/explore.rs new file mode 100644 index 0000000..8bbb7f2 --- /dev/null +++ b/nushell/crates/nu-explore/src/explore.rs @@ -0,0 +1,271 @@ +use crate::{ + PagerConfig, run_pager, + util::{create_lscolors, create_map}, +}; +use nu_ansi_term::{Color, Style}; +use nu_color_config::{StyleComputer, get_color_map}; +use nu_engine::command_prelude::*; +use nu_protocol::Config; + +/// A `less` like program to render a [`Value`] as a table. +#[derive(Clone)] +pub struct Explore; + +impl Command for Explore { + fn name(&self) -> &str { + "explore" + } + + fn description(&self) -> &str { + "Explore acts as a table pager, just like `less` does for text." + } + + fn signature(&self) -> nu_protocol::Signature { + // todo: Fix error message when it's empty + // if we set h i short flags it panics???? + + Signature::build("explore") + .input_output_types(vec![(Type::Any, Type::Any)]) + .named( + "head", + SyntaxShape::Boolean, + "Show or hide column headers (default true)", + None, + ) + .switch("index", "Show row indexes when viewing a list", Some('i')) + .switch( + "tail", + "Start with the viewport scrolled to the bottom", + Some('t'), + ) + .switch( + "peek", + "When quitting, output the value of the cell the cursor was on", + Some('p'), + ) + .category(Category::Viewers) + } + + fn extra_description(&self) -> &str { + r#"Press `:` then `h` to get a help menu."# + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let show_head: bool = call.get_flag(engine_state, stack, "head")?.unwrap_or(true); + let show_index: bool = call.has_flag(engine_state, stack, "index")?; + let tail: bool = call.has_flag(engine_state, stack, "tail")?; + let peek_value: bool = call.has_flag(engine_state, stack, "peek")?; + + let nu_config = stack.get_config(engine_state); + let style_computer = StyleComputer::from_config(engine_state, stack); + + let mut explore_config = ExploreConfig::from_nu_config(&nu_config); + explore_config.table.show_header = show_head; + explore_config.table.show_index = show_index; + explore_config.table.separator_style = lookup_color(&style_computer, "separator"); + + let lscolors = create_lscolors(engine_state, stack); + let cwd = engine_state.cwd(Some(stack)).map_or(String::new(), |path| { + path.to_str().unwrap_or("").to_string() + }); + + let config = PagerConfig::new( + &nu_config, + &explore_config, + &style_computer, + &lscolors, + peek_value, + tail, + &cwd, + ); + + let result = run_pager(engine_state, &mut stack.clone(), input, config); + + match result { + Ok(Some(value)) => Ok(PipelineData::Value(value, None)), + Ok(None) => Ok(PipelineData::Value(Value::default(), None)), + Err(err) => { + let shell_error = match err.downcast::() { + Ok(e) => e, + Err(e) => ShellError::GenericError { + error: e.to_string(), + msg: "".into(), + span: None, + help: None, + inner: vec![], + }, + }; + + Ok(PipelineData::Value( + Value::error(shell_error, call.head), + None, + )) + } + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Explore the system host information record", + example: r#"sys host | explore"#, + result: None, + }, + Example { + description: "Explore the output of `ls` without column names", + example: r#"ls | explore --head false"#, + result: None, + }, + Example { + description: "Explore a list of Markdown files' contents, with row indexes", + example: r#"glob *.md | each {|| open } | explore --index"#, + result: None, + }, + Example { + description: "Explore a JSON file, then save the last visited sub-structure to a file", + example: r#"open file.json | explore --peek | to json | save part.json"#, + result: None, + }, + ] + } +} + +#[derive(Debug, Clone)] +pub struct ExploreConfig { + pub table: TableConfig, + pub selected_cell: Style, + pub status_info: Style, + pub status_success: Style, + pub status_warn: Style, + pub status_error: Style, + pub status_bar_background: Style, + pub status_bar_text: Style, + pub cmd_bar_text: Style, + pub cmd_bar_background: Style, + pub highlight: Style, + /// if true, the explore view will immediately try to run the command as it is typed + pub try_reactive: bool, +} + +impl Default for ExploreConfig { + fn default() -> Self { + Self { + table: TableConfig::default(), + selected_cell: color(None, Some(Color::LightBlue)), + status_info: color(None, None), + status_success: color(Some(Color::Black), Some(Color::Green)), + status_warn: color(None, None), + status_error: color(Some(Color::White), Some(Color::Red)), + status_bar_background: color( + Some(Color::Rgb(29, 31, 33)), + Some(Color::Rgb(196, 201, 198)), + ), + status_bar_text: color(None, None), + cmd_bar_text: color(Some(Color::Rgb(196, 201, 198)), None), + cmd_bar_background: color(None, None), + highlight: color(Some(Color::Black), Some(Color::Yellow)), + try_reactive: false, + } + } +} +impl ExploreConfig { + /// take the default explore config and update it with relevant values from the nu config + pub fn from_nu_config(config: &Config) -> Self { + let mut ret = Self::default(); + + ret.table.column_padding_left = config.table.padding.left; + ret.table.column_padding_right = config.table.padding.right; + + let explore_cfg_hash_map = config.explore.clone(); + let colors = get_color_map(&explore_cfg_hash_map); + + if let Some(s) = colors.get("status_bar_text") { + ret.status_bar_text = *s; + } + + if let Some(s) = colors.get("status_bar_background") { + ret.status_bar_background = *s; + } + + if let Some(s) = colors.get("command_bar_text") { + ret.cmd_bar_text = *s; + } + + if let Some(s) = colors.get("command_bar_background") { + ret.cmd_bar_background = *s; + } + + if let Some(s) = colors.get("command_bar_background") { + ret.cmd_bar_background = *s; + } + + if let Some(s) = colors.get("selected_cell") { + ret.selected_cell = *s; + } + + if let Some(hm) = explore_cfg_hash_map.get("status").and_then(create_map) { + let colors = get_color_map(&hm); + + if let Some(s) = colors.get("info") { + ret.status_info = *s; + } + + if let Some(s) = colors.get("success") { + ret.status_success = *s; + } + + if let Some(s) = colors.get("warn") { + ret.status_warn = *s; + } + + if let Some(s) = colors.get("error") { + ret.status_error = *s; + } + } + + if let Some(hm) = explore_cfg_hash_map.get("try").and_then(create_map) { + if let Some(reactive) = hm.get("reactive") { + if let Ok(b) = reactive.as_bool() { + ret.try_reactive = b; + } + } + } + + ret + } +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct TableConfig { + pub separator_style: Style, + pub show_index: bool, + pub show_header: bool, + pub column_padding_left: usize, + pub column_padding_right: usize, +} + +const fn color(foreground: Option, background: Option) -> Style { + Style { + background, + foreground, + is_blink: false, + is_bold: false, + is_dimmed: false, + is_hidden: false, + is_italic: false, + is_reverse: false, + is_strikethrough: false, + is_underline: false, + prefix_with_reset: false, + } +} + +fn lookup_color(style_computer: &StyleComputer, key: &str) -> nu_ansi_term::Style { + style_computer.compute(key, &Value::nothing(Span::unknown())) +} diff --git a/nushell/crates/nu-explore/src/lib.rs b/nushell/crates/nu-explore/src/lib.rs new file mode 100644 index 0000000..a73ea14 --- /dev/null +++ b/nushell/crates/nu-explore/src/lib.rs @@ -0,0 +1,131 @@ +#![doc = include_str!("../README.md")] +mod commands; +mod default_context; +mod explore; +mod nu_common; +mod pager; +mod registry; +mod views; + +use anyhow::Result; +use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd}; +use crossterm::terminal::size; +pub use default_context::add_explore_context; +pub use explore::Explore; +use explore::ExploreConfig; +use nu_common::{collect_pipeline, has_simple_value}; +use nu_protocol::{ + PipelineData, Value, + engine::{EngineState, Stack}, +}; +use pager::{Page, Pager, PagerConfig}; +use registry::CommandRegistry; +use views::{BinaryView, Orientation, Preview, RecordView}; + +mod util { + pub use super::nu_common::{create_lscolors, create_map}; +} + +fn run_pager( + engine_state: &EngineState, + stack: &mut Stack, + input: PipelineData, + config: PagerConfig, +) -> Result> { + let mut p = Pager::new(config.clone()); + let commands = create_command_registry(); + + let is_record = matches!(input, PipelineData::Value(Value::Record { .. }, ..)); + let is_binary = matches!( + input, + PipelineData::Value(Value::Binary { .. }, ..) | PipelineData::ByteStream(..) + ); + + if is_binary { + p.show_message("For help type :help"); + + let view = binary_view(input, config.explore_config)?; + return p.run(engine_state, stack, Some(view), commands); + } + + let (columns, data) = collect_pipeline(input)?; + + let has_no_input = columns.is_empty() && data.is_empty(); + if has_no_input { + return p.run(engine_state, stack, help_view(), commands); + } + + p.show_message("For help type :help"); + + if let Some(value) = has_simple_value(&data) { + let text = value.to_abbreviated_string(config.nu_config); + let view = Some(Page::new(Preview::new(&text), false)); + return p.run(engine_state, stack, view, commands); + } + + let view = create_record_view(columns, data, is_record, config); + p.run(engine_state, stack, view, commands) +} + +fn create_record_view( + columns: Vec, + data: Vec>, + // wait, why would we use RecordView for something that isn't a record? + is_record: bool, + config: PagerConfig, +) -> Option { + let mut view = RecordView::new(columns, data, config.explore_config.clone()); + if is_record { + view.set_top_layer_orientation(Orientation::Left); + } + + if config.tail { + if let Ok((w, h)) = size() { + view.tail(w, h); + } + } + + Some(Page::new(view, true)) +} + +fn help_view() -> Option { + Some(Page::new(HelpCmd::view(), false)) +} + +fn binary_view(input: PipelineData, config: &ExploreConfig) -> Result { + let data = match input { + PipelineData::Value(Value::Binary { val, .. }, _) => val, + PipelineData::ByteStream(bs, _) => bs.into_bytes()?, + _ => unreachable!("checked beforehand"), + }; + + let view = BinaryView::new(data, config); + + Ok(Page::new(view, true)) +} + +fn create_command_registry() -> CommandRegistry { + let mut registry = CommandRegistry::new(); + create_commands(&mut registry); + create_aliases(&mut registry); + + registry +} + +fn create_commands(registry: &mut CommandRegistry) { + registry.register_command_view(NuCmd::new(), true); + registry.register_command_view(TableCmd::new(), true); + + registry.register_command_view(ExpandCmd::new(), false); + registry.register_command_view(TryCmd::new(), false); + registry.register_command_view(HelpCmd::default(), false); + + registry.register_command_reactive(QuitCmd); +} + +fn create_aliases(registry: &mut CommandRegistry) { + registry.create_aliases("h", HelpCmd::NAME); + registry.create_aliases("e", ExpandCmd::NAME); + registry.create_aliases("q", QuitCmd::NAME); + registry.create_aliases("q!", QuitCmd::NAME); +} diff --git a/nushell/crates/nu-explore/src/nu_common/command.rs b/nushell/crates/nu-explore/src/nu_common/command.rs new file mode 100644 index 0000000..c86c686 --- /dev/null +++ b/nushell/crates/nu-explore/src/nu_common/command.rs @@ -0,0 +1,119 @@ +use nu_engine::eval_block; +use nu_parser::parse; +use nu_protocol::{ + OutDest, PipelineData, ShellError, Value, + debugger::WithoutDebug, + engine::{EngineState, Redirection, Stack, StateWorkingSet}, +}; +use std::sync::Arc; + +pub fn run_command_with_value( + command: &str, + input: &Value, + engine_state: &EngineState, + stack: &mut Stack, +) -> Result { + if is_ignored_command(command) { + return Err(ShellError::GenericError { + error: "Command ignored".to_string(), + msg: "the command is ignored".to_string(), + span: None, + help: None, + inner: vec![], + }); + } + + let pipeline = PipelineData::Value(input.clone(), None); + let pipeline = run_nu_command(engine_state, stack, command, pipeline)?; + if let PipelineData::Value(Value::Error { error, .. }, ..) = pipeline { + Err(ShellError::GenericError { + error: "Error from pipeline".to_string(), + msg: error.to_string(), + span: None, + help: None, + inner: vec![*error], + }) + } else { + Ok(pipeline) + } +} + +pub fn run_nu_command( + engine_state: &EngineState, + stack: &mut Stack, + cmd: &str, + current: PipelineData, +) -> std::result::Result { + let mut engine_state = engine_state.clone(); + eval_source2(&mut engine_state, stack, cmd.as_bytes(), "", current) +} + +pub fn is_ignored_command(command: &str) -> bool { + let ignore_list = ["clear", "explore", "exit"]; + + for cmd in ignore_list { + if command.starts_with(cmd) { + return true; + } + } + + false +} + +fn eval_source2( + engine_state: &mut EngineState, + stack: &mut Stack, + source: &[u8], + fname: &str, + input: PipelineData, +) -> Result { + let (mut block, delta) = { + let mut working_set = StateWorkingSet::new(engine_state); + let output = parse( + &mut working_set, + Some(fname), // format!("entry #{}", entry_num) + source, + false, + ); + + if let Some(err) = working_set.parse_errors.first() { + return Err(ShellError::GenericError { + error: "Parse error".to_string(), + msg: err.to_string(), + span: None, + help: None, + inner: vec![], + }); + } + + (output, working_set.render()) + }; + + // We need to merge different info other wise things like PIPEs etc will not work. + if let Err(err) = engine_state.merge_delta(delta) { + return Err(ShellError::GenericError { + error: "Merge error".to_string(), + msg: err.to_string(), + span: None, + help: None, + inner: vec![err], + }); + } + + // eval_block outputs all expressions except the last to STDOUT; + // we don't won't that. + // + // So we LITERALLY ignore all expressions except the LAST. + if block.len() > 1 { + let range = ..block.pipelines.len() - 1; + // Note: `make_mut` will mutate `&mut block: &mut Arc` + // for the outer fn scope `eval_block` + Arc::make_mut(&mut block).pipelines.drain(range); + } + + let stack = &mut stack.push_redirection( + Some(Redirection::Pipe(OutDest::PipeSeparate)), + Some(Redirection::Pipe(OutDest::PipeSeparate)), + ); + eval_block::(engine_state, stack, &block, input) +} diff --git a/nushell/crates/nu-explore/src/nu_common/lscolor.rs b/nushell/crates/nu-explore/src/nu_common/lscolor.rs new file mode 100644 index 0000000..7a3400d --- /dev/null +++ b/nushell/crates/nu-explore/src/nu_common/lscolor.rs @@ -0,0 +1,129 @@ +use std::path::Path; + +use super::NuText; +use lscolors::LsColors; +use nu_ansi_term::{Color, Style}; +use nu_engine::env_to_string; +use nu_path::{expand_path_with, expand_to_real_path}; +use nu_protocol::engine::{EngineState, Stack}; +use nu_utils::get_ls_colors; + +pub fn create_lscolors(engine_state: &EngineState, stack: &Stack) -> LsColors { + let colors = stack + .get_env_var(engine_state, "LS_COLORS") + .and_then(|v| env_to_string("LS_COLORS", v, engine_state, stack).ok()); + + get_ls_colors(colors) +} + +/// Colorizes any columns named "name" in the table using LS_COLORS +pub fn lscolorize(header: &[String], data: &mut [Vec], cwd: &str, lscolors: &LsColors) { + for (col, col_name) in header.iter().enumerate() { + if col_name != "name" { + continue; + } + + for row in data.iter_mut() { + let (path, text_style) = &mut row[col]; + + let style = get_path_style(path, cwd, lscolors); + if let Some(style) = style { + *text_style = text_style.style(style); + } + } + } +} + +fn get_path_style(path: &str, cwd: &str, ls_colors: &LsColors) -> Option