Compare commits

...

67 Commits

Author SHA1 Message Date
36427a7434 update to rust version 1.87.0 (#16437)
The PR upgrades nushell to rust version 1.87.0.

## Dev overview from clippy
- I added `result_large_err` to clippy in the root Cargo.toml to avoid
the warnings (and a few places in plugins). At some point a more proper
fix, perhaps boxing these, will need to be performed. This PR is to just
get us over the hump.
- I boxed a couple areas in some commands
- I changed `rdr.bytes()` to `BufReader::new(rdr).bytes()` in nu-json

## Release notes summary - What our users need to know
Users can use rust version 1.87.0 to compile nushell now

## Tasks after submitting
N/A
2025-08-14 11:27:34 -05:00
daf52ba5c8 refactor: run env_change hooks before pre_prompt hooks (#16356)
Change the order of hook evaluations, run `env_change` before `pre_prompt`.
New order of execution is: `env_change` -> `pre_prompt` -> `PROMPT_COMMAND`
2025-08-14 02:11:05 +03:00
2f7f00001d feat(std/bench) add osc 9;4 progress bar (#16245)
# Description
Using `osc 9;4`, `bench` shows a progress bar or circle on supported
terminals, every 10 timing rounds to not degrade performance too much.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2025-08-13 16:49:39 -04:00
ee7334a772 feat(get,select,reject): add --ignore-case which interprets cell-paths case insensitively, analogous to --optional (#16401)
# Description
Follow up to #16007

Also added some examples for existing flags which were previously
missing.

After the deprecation period of `--ignore-errors (-i)`, `-i` will be
used as the short form of this flag.

# User-Facing Changes
`get`, `select`, `reject` commands now have a `--ignore-case` flag,
which makes the commands interpret all cell-path arguments as completely
case insensitive.

# Tests + Formatting
+1

# After Submitting
Set a reminder for the `--ignore-errors` deprecation and using `-i` as
the short flag. Maybe we can make PRs in advance for future versions and
mark them with GitHub's milestones.
2025-08-13 16:45:33 -04:00
3fe9c7c00c feat(each): noop on single null input, map-null equivalent (#16396)
# Description
Basically, now `null | each { "something" }` will be `null` instead of
`"something"`. Thanks to this, `each` can be used to map values similar
to `map-null` custom commands, for example:
- Before
```nu
let args = if $delay != null {
    ["--delay" ($delay | format duration sec | parse '{secs} {_}' | get 0.secs)]
} else {
    []
}
```
- After
```nu
let args = (
    $delay
    | each { ["--delay" ($in | format duration sec | parse '{secs} {_}' | get 0.secs)] }
    | default []
)
```

Please let me know if this change messes something up I'm not seeing.
# User-Facing Changes
- Before
```nu
→ null | each { "something" }
something
```
- After
```nu
→ null | each { "something" }
null
```
# Tests + Formatting
Added a test to check for this.

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->

---------

Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
2025-08-13 15:22:24 -05:00
43992f5b6f Rework PR template (#16412) 2025-08-13 22:10:47 +02:00
91e72ae8b4 Validate std/random dice args (#16430)
Fixes #16429
2025-08-13 21:50:49 +02:00
da54ed8ea1 docs: document undocumented Signature methods (#16417)
Just a small PR which documents methods. I got confused what `rest` did
while working on a plugin, so I decided to document all of them.
2025-08-13 20:58:30 +02:00
3eabc83c61 build(deps): bump crate-ci/typos from 1.35.1 to 1.35.4 (#16424) 2025-08-13 18:55:36 +00:00
cc4a4a11f0 build(deps): bump sysinfo from 0.36.0 to 0.36.1 (#16423) 2025-08-13 18:55:11 +00:00
d53e16748d build(deps): bump rayon from 1.10.0 to 1.11.0 (#16422) 2025-08-13 18:54:22 +00:00
4ead4ce4d6 feat: move random dice to std (#16420)
# Description

As per the suggestion in #16350, this PR moves `random dice` to std.
It's basically a thin wrapper over `random int` already.

# User-Facing Changes (deprecations)

## `random dice` moved to `std`

The `random dice` command has been rewritten in Nushell and moved to the
standard library. The `random dice` built-in is still available with a
deprecation error, but will be removed in 0.108. The new command can be
used as follows:

```nushell
use std/random

random dice
```

It's behavior, parameters, and defaults are the same.


# After Submitting

Update documentation to reflect the change.

Closes #16350
2025-08-13 20:45:45 +02:00
31606a8fe1 Kill background jobs on interrupt (#16285)
# Description
This PR kills all background jobs on interrupt, as a fix for
https://github.com/nushell/nushell/issues/15947.

# User-Facing Changes
If you run the following: `job spawn { print "job spawned"; ^sleep
infinity }; ^sleep infinity`, then hit ctrl-c, the current behavior is
that the `sleep` process from the job will not be killed, it will
reparented to init. With this change, the process will be killed on
ctrl-c.

# Tests + Formatting
I was unsure of the best way to write a test for this.

# After Submitting

---------

Co-authored-by: 132ikl <132@ikl.sh>
2025-08-13 13:11:40 -04:00
7133a04e2f Improve wrong flag help (#16427)
# Description

Currently, when Nushell encounters an unknown flag, it prints all
options in the help string. This is pretty verbose and uses the
`formatted_flags` signature method, which isn't used anywhere else. This
commit refactors the parser to use `did_you_mean` instead, which only
suggest one closest option or sends the user to `help` if nothing close
is found.


# User-Facing Changes (Bug fixes and other changes)

## Improved error messages for misspelled flags

Previously, the help text for a missing flag would list all of them,
which could get verbose on a single line:

```nushell
~> ls --full-path
Error: nu::parser::unknown_flag

  × The `ls` command doesn't have flag `full-path`.
   ╭─[entry #8:1:4]
 1 │ ls --full-path
   ·    ─────┬─────
   ·         ╰── unknown flag
   ╰────
  help: Available flags: --help(-h), --all(-a), --long(-l), --short-names(-s), --full-paths(-f), --du(-d), --directory(-D), --mime-type(-m), --threads(-t). Use
        `--help` for more information.
```

The new error message only suggests the closest flag:

```nushell
> ls --full-path
Error: nu::parser::unknown_flag

  × The `ls` command doesn't have flag `full-path`.
   ╭─[entry #23:1:4]
 1 │ ls --full-path
   ·    ─────┬─────
   ·         ╰── unknown flag
   ╰────
  help: Did you mean: `--full-paths`?
```


---

Closes #16418
2025-08-13 06:25:18 -05:00
79a6c78032 implement FromValue for std::time::Duration and refactor relevant commands to utilize that (#16414)
# Description

- Implemented `FromValue` for `std::time::Duration`.
- It only converts positive `Value::Duration` values, negative ones
raise `ShellError::NeedsPositiveValue`.
- Refactor `job recv` and `watch` commands to use this implementation
rather than handling it ad-hoc.
- Simplified `watch`'s `debounce` & `debounce-ms` and factored it to a
function. Should make removing `debounce-ms` after its deprecation
period ends.
- `job recv` previously used a numeric cast (`i64 as u64`) which would
result in extremely long duration values rather than raising an error
when negative duration arguments were given.

# User-Facing Changes

Changes in error messages:
- Providing the wrong type (bypassing parse time type checking):
  - Before
    ```
    Error: nu:🐚:type_mismatch

      × Type mismatch.
       ╭─[entry #40:1:9]
     1 │ watch . --debounce (1 | $in) {|| }
       ·         ──────────┬─────────
       ·                   ╰── Debounce duration must be a duration
       ╰────
    ```
  - After
    ```
    Error: nu:🐚:cant_convert

      × Can't convert to duration.
       ╭─[entry #2:1:9]
     1 │ watch . --debounce (1 | $in) {|| }
       ·         ──────────┬─────────
       ·                   ╰── can't convert int to duration
       ╰────
    ```
- Providing a negative duration value:
  - Before
    ```
    Error: nu:🐚:type_mismatch

      × Type mismatch.
       ╭─[entry #41:1:9]
     1 │ watch . --debounce -100ms {|| }
       ·         ────────┬────────
       ·                 ╰── Debounce duration is invalid
       ╰────
    ```
  - After
    ```
    Error: nu:🐚:needs_positive_value

      × Negative value passed when positive one is required
       ╭─[entry #4:1:9]
     1 │ watch . --debounce -100ms {|| }
       ·         ────────┬────────
       ·                 ╰── use a positive value
       ╰────
    ```
2025-08-12 22:25:23 +02:00
5478bdff0e feat: null values can be spread as if they are empty lists or records. (#16399)
# Description
Spread operator `...` treats `null` values as empty collections,
, whichever of list or record is appropriate.

# User-Facing Changes
`null` values can be used with the spread operator (`...`)
2025-08-11 23:47:31 +03:00
a4711af952 feat: impl<B> for (From/Into)Value for Cow<'_, B> where B::Owned: (From/Into)Value (#16380)
# Description
Implements `FromValue` and `IntoValue` for `Cow`, which makes it easy to
use it in types that need to implement these traits.

I don't think it will have a significant impact, but it can let us avoid
allocations where we can use static values.

# Tests + Formatting
No need, the implementations are delegated to `B::Owned`.

Co-authored-by: Bahex <Bahex@users.noreply.github.com>
2025-08-11 19:47:50 +02:00
751ef6e8da [parser] Improve type errors for boolean literals (#16408)
Currently, strings `true` and `false` are intercepted early in parsing.
Previously, if they didn't match the expected shape, the parse error
said "expected non-boolean value". This PR changes the message to say
"expected <shape>".

Because the parsing error requires a `&'static str` I had to add a
`to_str(&self) -> &'static str` method on `SyntaxShape`. It's a bit
crude for more complex shapes: it simply says `keyword`, `list`,
`table`, and so on for them, without exposing the underlying structure.

Fixes #16406
2025-08-11 11:59:27 -05:00
038f8f85ed Fix missing $env.PATH unhelpful error and breaking CMD builtins (#16410)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->
Fixes #16409 

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
After running `hide-env PATH`, there is a more helpful error and CMD
builtins still work.
<img width="503" height="217" alt="image"
src="https://github.com/user-attachments/assets/a43180f9-5bc2-43bd-9773-aa9ad1818386"
/>
<img width="779" height="253" alt="image"
src="https://github.com/user-attachments/assets/03b59209-f9a9-4c61-9ea2-8fbdc27b8d4b"
/>


# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->

---------

Co-authored-by: 132ikl <132@ikl.sh>
2025-08-11 12:55:22 -04:00
4245c67ce3 Add --raw flag to to html (#16373)
Works towards #16347 however more work may be required first

# Description
Adds a `--raw` flag to `to html`. This stops the resulting html content
being escaped
# User-Facing Changes

# Tests + Formatting
# After Submitting
2025-08-11 21:31:24 +08:00
e56879e588 fix(parser): missing span of short flag that requires a value (#16376)
Fixes #16375

# Description

# User-Facing Changes

Bug fix

# Tests + Formatting

+0.5
2025-08-09 23:50:55 +02:00
c75e7bfbd3 Fix watch return type (#16400)
Refs
https://discord.com/channels/601130461678272522/615329862395101194/1403760147985207487

# Description

Currently `watch` doesn't normally return, ever. The only way to stop it
is to abort with `Ctrl+C` (or some internal error happens), so it never
produces a usable pipeline output. Since nu doesn't have `never` type
yet, `nothing` is the closest thing we can use.

# User-Facing Changes

Users may start to get type errors if they used `watch .... | something`
and the `something` did not accept `nothing`.

# Tests + Formatting

All pass.
2025-08-09 23:43:51 +02:00
e8579a9268 Add examples for receiving data from a job (#16372)
# Description

The existing examples cover how to send data to a job, but I think it
will be much more common to want to receive data from a job.

# User-Facing Changes

Just documentation, though it may be worth highlighting anyway. I really
thought for a while that this was not possible yet. See also my book PR
https://github.com/nushell/nushell.github.io/pull/2006 (`job send` and
`job recv` were not documented in the book at all).
2025-08-09 07:39:44 +08:00
3dead9a001 Respect $env.LC_ALL and $env.LANG in format date (#16369)
Refs
https://github.com/nushell/nushell/issues/16368#issuecomment-3160728758

# Description

Respect user preference for date/time formats, in a more compatible way.
Environment variable order is taken from
https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html.
Previously, only `$env.LC_TIME` was consulted to select the locale.

# User-Facing Changes

Users will be able to specify the format preference via `$env.LANG` and
override it with `$env.LC_ALL`.

# Tests + Formatting

All pass.
2025-08-09 07:36:34 +08:00
0b106789a7 build(deps): bump mach2 from 0.4.2 to 0.4.3 (#16363)
Bumps [mach2](https://github.com/JohnTitor/mach2) from 0.4.2 to 0.4.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/JohnTitor/mach2/releases">mach2's
releases</a>.</em></p>
<blockquote>
<h2>0.4.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Add <code>time_value</code> by <a
href="https://github.com/ldm0"><code>@​ldm0</code></a> in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/25">JohnTitor/mach2#25</a></li>
<li>Add <code>mach-o/dyld.h</code> items by <a
href="https://github.com/JohnTitor"><code>@​JohnTitor</code></a> in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/32">JohnTitor/mach2#32</a></li>
<li>Add <code>struct</code> prefix on all the structs in C by <a
href="https://github.com/JohnTitor"><code>@​JohnTitor</code></a> in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/33">JohnTitor/mach2#33</a></li>
<li>chore(ci): Update macOS version by <a
href="https://github.com/JohnTitor"><code>@​JohnTitor</code></a> in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/37">JohnTitor/mach2#37</a></li>
<li>Configure Renovate by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/36">JohnTitor/mach2#36</a></li>
<li>chore(deps): pin dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/39">JohnTitor/mach2#39</a></li>
<li>chore: Use original ctest by <a
href="https://github.com/JohnTitor"><code>@​JohnTitor</code></a> in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/40">JohnTitor/mach2#40</a></li>
<li>chore: Prepare 0.4.3 release by <a
href="https://github.com/JohnTitor"><code>@​JohnTitor</code></a> in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/41">JohnTitor/mach2#41</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/ldm0"><code>@​ldm0</code></a> made their
first contribution in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/25">JohnTitor/mach2#25</a></li>
<li><a href="https://github.com/renovate"><code>@​renovate</code></a>
made their first contribution in <a
href="https://redirect.github.com/JohnTitor/mach2/pull/36">JohnTitor/mach2#36</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/JohnTitor/mach2/compare/0.4.2...0.4.3">https://github.com/JohnTitor/mach2/compare/0.4.2...0.4.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ed16d350ab"><code>ed16d35</code></a>
Merge pull request <a
href="https://redirect.github.com/JohnTitor/mach2/issues/41">#41</a>
from JohnTitor/chore/0.4.3</li>
<li><a
href="86b3f278c4"><code>86b3f27</code></a>
chore: Prepare 0.4.3 release</li>
<li><a
href="6b9a8bcdca"><code>6b9a8bc</code></a>
Merge pull request <a
href="https://redirect.github.com/JohnTitor/mach2/issues/40">#40</a>
from JohnTitor/chore/ctest</li>
<li><a
href="ae47e1935b"><code>ae47e19</code></a>
chore: Use original ctest</li>
<li><a
href="52f3edbfaf"><code>52f3edb</code></a>
Merge pull request <a
href="https://redirect.github.com/JohnTitor/mach2/issues/39">#39</a>
from JohnTitor/renovate/pin-dependencies</li>
<li><a
href="ceee600d4b"><code>ceee600</code></a>
chore(deps): pin dependencies</li>
<li><a
href="ac9f277587"><code>ac9f277</code></a>
chore(renovate): Enable GHA digests helper</li>
<li><a
href="3f3769660c"><code>3f37696</code></a>
Merge pull request <a
href="https://redirect.github.com/JohnTitor/mach2/issues/36">#36</a>
from JohnTitor/renovate/configure</li>
<li><a
href="ab4d126d28"><code>ab4d126</code></a>
Add renovate.json</li>
<li><a
href="d156e25e99"><code>d156e25</code></a>
Merge pull request <a
href="https://redirect.github.com/JohnTitor/mach2/issues/37">#37</a>
from JohnTitor/chore/update-macos-version</li>
<li>Additional commits viewable in <a
href="https://github.com/JohnTitor/mach2/compare/0.4.2...0.4.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mach2&package-manager=cargo&previous-version=0.4.2&new-version=0.4.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-09 07:30:56 +08:00
bf83756562 Fix example result span (#16395)
Refs
https://discord.com/channels/601130461678272522/614593951969574961/1403435414416654427

# Description

Previously Example result values would have a test span, which would
cause hard to understand errors for the code that uses `scope commands`.

Now they will have the span that points to `scope commands` invocation.

# User-Facing Changes

Errors referencing example results will get slightly better.

# Tests + Formatting

All pass.
2025-08-09 07:29:27 +08:00
06fa1784c1 Clarify that input's history feature uses reedline (#16334)
Follow up to this commit @sholderbach made on my PR #16329:
f21350ec88 (diff-5cab4dac5ced236548db9fbf6cd0e9d250ba12317bb916ec26603054ce9144a7)
2025-08-08 22:20:42 +02:00
e6d673c39e fix (std/help): fix bug and use is-not-empty (#16394)
# Description
In #16354, I introduced a bug thinking that `table -e` would always
return a string, so I fix it here restoring the `to text`. While I was
at it, I changed all instances of `if not ($var | is-empty)` to `if
($var | is-not-empty)` to improve readability.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2025-08-08 13:58:33 -05:00
c4fcd54573 nu-table: Fix header on border index coloring (#16377)
Sorry for delay

close #16345

<img width="519" height="263" alt="image"
src="https://github.com/user-attachments/assets/e5fe2a23-5a47-4c18-933f-6cf936ea702c"
/>

About the incorrect alignment of the index header `#` it shall be fixed
with the next `tabled` release.

cc: @fdncred
2025-08-07 08:19:39 -05:00
4e56cd5fc4 fix(parser): external argument with subexpressions (#16346)
Fixes: #16040

# Description

TBH, I not a fan of this whole `parse_external_string` idea.
Maybe I lack some of the background knowledge here, but I don't see why
we choose not to
1. parse external arguments the same way as internal ones
2. treat them literally at runtime if necessary

Tests: +1
2025-08-06 22:17:58 +03:00
0e3ca7b355 build(deps): bump fancy-regex from 0.14.0 to 0.16.1 (#16365) 2025-08-06 05:26:49 +00:00
2b69bd9b6d build(deps): bump crate-ci/typos from 1.34.0 to 1.35.1 (#16360) 2025-08-06 12:27:52 +08:00
3a82c6c88d Fix panic in unit parsing with non-UTF8 code (#16355)
# Description
Trying to parse non-UTF8 data as a value with unit (part of every
literal parse) introduced a replacement character which shifted the span
information so the indices where incorrect and triggered
a panic.

This has been resolved by removing a bad `String::from_utf8_lossy`

# User-Facing Changes
One less possible panic

# Tests + Formatting
Added a test with the original reproducer from fuzzing:

File with `0\xffB` where the `\xff` represents the non utf-8 char `FF`
run as a script to trigger
2025-08-05 22:08:33 +02:00
61a89c1834 Fully qualify the sqlite path for into sqlite (#16349)
- related #16258

# Description


In #16258 we had some trouble getting tests to work properly. After some
investigation with @WindSoilder we figured out that `Table::new` inside
`into_sqlite.rs` did not respect the `$env.PWD`. The underlying
`open_sqlite_db` and in that `Connection::open` respects the current
working directory. That one is updated via `cd` but not necessarily in
tests (and we should not try that). In this PR I join the `$env.PWD`
with the path passed to `into sqlite`.

This PR also adds a test for the auto conversion from #16258.

# User-Facing Changes

Should be none, some edge cases might be fixed now.
2025-08-05 22:02:33 +02:00
fcdc7f3d83 fix(std/help): trim example results and fix binary examples (#16354)
# Description
Added trimming to the example results like in the normal `help`, and
also changed the logic for binary examples to remove the weird spacing.
- Before:
<img width="998" height="919" alt="image"
src="https://github.com/user-attachments/assets/03f18f45-5b12-41bc-b495-232bcf899964"
/>

- After:
<img width="959" height="720" alt="image"
src="https://github.com/user-attachments/assets/894dc622-c603-467c-8904-aef582a82b0a"
/>

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2025-08-05 13:21:23 -05:00
2b70d27cdf fix(help): don't trim example result beginning (#16353)
# Description
The `help` command, when printing the examples with results, trims the
first line and it appears unindented compared to the following lines.

- Before:
<img width="1110" height="346" alt="image"
src="https://github.com/user-attachments/assets/3487381d-3631-49c9-bb0e-f7ad958b7291"
/>

- After:
<img width="1123" height="339" alt="image"
src="https://github.com/user-attachments/assets/acb45afd-0492-49d2-a5cb-5130bbb4cf94"
/>

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2025-08-05 13:20:37 -05:00
8c2af9941c feat(std-rfc/str): add str align (#16062) 2025-08-06 00:21:23 +08:00
f015409253 [nu-std] std-rfc/random: add random choice (#16270)
# Description

Adds `random choice` suggested in #16241.

# User-Facing Changes

New `random` module in `std-rfc` with the `choice` subcommand.

# Tests + Formatting

Unsure how do to do tests. Sampling and a histogram should be enough,
but they'll be non-deterministic.

# After Submitting

<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->

---------

Co-authored-by: sholderbach <sholderbach@users.noreply.github.com>
2025-08-04 20:36:28 +02:00
f33d952adf 'stor create/insert/open' & 'query db' now support JSON columns (#16258)
Co-authored-by: Tim 'Piepmatz' Hesse <git+github@cptpiepmatz.de>
2025-08-04 16:58:21 +02:00
9f4c3a1d10 Fix path relative-to for case-insensitive filesystems (#16310)
fixes #16205 

# Description

1. **Adds fallback**: On case-insensitive filesystems (Windows, macOS),
falls back to case-insensitive comparison when the standard comparison
fails
2. **Maintains filesystem semantics**: Only uses case-insensitive
comparison on platforms where it's appropriate

## Before:
```console
$> "/etc" | path relative-to "/Etc"
Error: nu:🐚:cant_convert

  × Can't convert to prefix not found.
   ╭─[entry #33:1:1]
 1 │ "/etc" | path relative-to "/Etc"
   · ───┬──
   ·    ╰── can't convert string to prefix not found
   ╰────
```

## After:
For Windows and macOS:
```console
$> "/etc" | path relative-to "/Etc" | debug -v
""
```
2025-08-04 22:32:31 +08:00
4f9c0775d9 Change the behavior of --ignore-case and --multiline options for find (#16323)
# Description

Changes the behavior of `--ignore-case` and `--multiline` options for
`find`, to make them more consistent between regex mode and search term
mode, and to enable more options for using find.

# User-Facing Changes

Search term mode is now case-sensitive by default.

`--ignore-case` will make the search case-insensitive in search term
mode. In regex mode, the previous behavior of adding a (?i) flag to the
regex is preserved.

`--multiline` will no longer add a (?m) flag in regex mode. Instead, it
will make the search not split multi-line strings into lists of lines.

closes #16317
closes #16022
2025-08-04 22:27:00 +08:00
d528bb713b Fix UTF-8 multibyte handling in explore inputs (#16325)
# Description
`explore` was reading the byte length for computing:
- `:` command input cursor positions
- `/`/`?` search mode cursor positions
- `:try` input box cursor positions
- search highlighting

Fixed this for the majority of cases by using `unicode_width`, this is
only best effort as terminals don't need to follow those expectations
but for the most cases (traditional language scripts etc.) this should
lead to better result. The only way around the uncertainty would be to
perform the highlighting/cursor marking as we go, but this may not be as
compatible with the `ratatui` framework.

Closes #16312

# User-Facing Changes
Fixed cursor position and search highlighting for non-ASCII characters,
with the caveat mentioned above.

# Tests + Formatting
Manually tested
2025-08-03 21:20:35 +02:00
7cc1a86459 typo: help format filesize has a wrong example (#16336)
Just a small one letter typo in the help command. It should be `kB`

<img width="943" height="688" alt="image"
src="https://github.com/user-attachments/assets/fcca3978-cc0d-483f-b74e-465743213b76"
/>
2025-08-03 13:27:03 +02:00
dfbd98013d Add send: vichangemode to reedline config (#16327)
# Description
Allows custom bindings (non-chord) to send a `edit_mode: vi` mode change
via the new `ReedlineEvent::ViChangeMode`
Takes https://github.com/nushell/reedline/pull/932

# User-Facing Changes
You can now set bindings which change the Vi mode. (This still has the
same rules for defining the key-combination: Only modifiers and single
keys are supported)
To do so send a `vichangemode` event with the `mode` field to set
`normal`, `insert`, or `visual`

```nushell
$env.config.keybindings ++=
	[{
	    name: modechangetest
	    modifier: control
	    keycode: "char_["
	    mode: [vi_normal, vi_insert]
	    event: {send: vichangemode, mode: normal}
	}]

```
2025-08-03 13:23:55 +02:00
2c9f6acc03 Forgo full build in the cargo hack wf (#16328)
We ran out of disk space on Github actions with the build part of this
workflow. We hope that `check` should catch the worst offenders.
2025-08-02 21:04:43 +02:00
007d15ed9f Add multiline example for input command (#16329)
# Description

Adds an example that documents how to use `input --reedline` to collect
multiple lines of input from the user

I also removed an extraneous and inconsistent space in the following
example.

# User-Facing Changes

Documentation addition

# Tests + Formatting

I did not run any tests or autoformatters because of the docs-only
nature of the change, and the fact that I copy-pasted the format from an
existing example. If the autoformatter is unhappy, I apologize.

# After Submitting

This PR should automatically update the docs site at the next release,
so no need to do anything there.

---------

Co-authored-by: Stefan Holderbach <sholderbach@users.noreply.github.com>
2025-08-02 21:04:20 +02:00
3e37922537 Bump ureq, get redirect history. (#16078) 2025-08-02 13:55:37 +02:00
1274d1f7e3 Add -h/--help flag to testbin (#16196)
# Description
As title, this pr introduce `-h` flag to testbin, so if I want to see
which testbin I should use, I don't need to look into source code.

### About the change
I don't know if there is any way to get docstring of a function inside
rust code. So I created a trait, and put docstring into it's `help`
method:
```rust
pub trait TestBin {
    // the docstring of original functions are moved here.
    fn help(&self) -> &'static str;
    fn run(&self);
}
```
Take `cococo` testbin as example, the changes are:
```
original cococo function --> Cococo struct, then
1. put the body of `cococo` function into `run` method
2. put the docstring of `cococo` function into `help` method
```

# User-Facing Changes

`-h/--help` flag in testbin is enabled.
```
> nu --testbin -h
Usage: nu --testbin <bin>
<bin>:
chop -> With no parameters, will chop a character off the end of each line
cococo -> Cross platform echo using println!()(e.g: nu --testbin cococo a b c)
echo_env -> Echo's value of env keys from args(e.g: nu --testbin echo_env FOO BAR)
echo_env_mixed -> Mix echo of env keys from input(e.g: nu --testbin echo_env_mixed out-err FOO BAR; nu --testbin echo_env_mixed err-out FOO BAR)
echo_env_stderr -> Echo's value of env keys from args to stderr(e.g: nu --testbin echo_env_stderr FOO BAR)
echo_env_stderr_fail -> Echo's value of env keys from args to stderr, and exit with failure(e.g: nu --testbin echo_env_stderr_fail FOO BAR)
fail -> Exits with failure code 1(e.g: nu --testbin fail)
iecho -> Another type of echo that outputs a parameter per line, looping infinitely(e.g: nu --testbin iecho 3)
input_bytes_length -> Prints the number of bytes received on stdin(e.g: 0x[deadbeef] | nu --testbin input_bytes_length)
meow -> Cross platform cat (open a file, print the contents) using read_to_string and println!()(e.g: nu --testbin meow file.txt)
meowb -> Cross platform cat (open a file, print the contents) using read() and write_all() / binary(e.g: nu --testbin meowb sample.db)
nonu -> Cross platform echo but concats arguments without space and NO newline(e.g: nu --testbin nonu a b c)
nu_repl -> Run a REPL with the given source lines
relay -> Relays anything received on stdin to stdout(e.g: 0x[beef] | nu --testbin relay)
repeat_bytes -> A version of repeater that can output binary data, even null bytes(e.g: nu --testbin repeat_bytes 003d9fbf 10)
repeater -> Repeat a string or char N times(e.g: nu --testbin repeater a 5)
```

# Tests + Formatting
None, all existed tests can guarantee the behavior of testbins doesn't
change.

# After Submitting
NaN
2025-08-02 10:48:07 +08:00
da9615f971 Fix parse-time pipeline type checking to support multiple output types for same input type (#16111)
# Description
Fixes #15485

This PR changes pipeline checking to keep track of all possible output
types instead of only first type matching input type which appears in
the input/output types. For example, in this command:
```nushell
def foo []: [int -> string, int -> record] {
  # ...
}
```
An `int` input to the command may result in a string or a record to be
output. Before this PR, Nushell would always assume that an `int` input
would cause a `string` output because it's the first matching
input/output type pair. This would cause issues during type checking
where the parser would incorrectly determine the output type. After this
PR, Nushell considers the command to output either a string or a record.

# User-Facing Changes
* Parse-time pipeline type checking now properly supports commands with
multiple pipeline output types for the same pipeline input type

# Tests + Formatting
Added a couple tests

# After Submitting
N/A

---------

Co-authored-by: Bahex <Bahex@users.noreply.github.com>
2025-08-02 09:35:25 +08:00
eb8d2d3206 Refactor: introduce 2 associated functions to PipelineData (#16233)
# Description
As title: this pr is try to introduce 2 functions to `PipelineData`:
1. PipelineData::list_stream --> create a PipelineData::ListStream
2. PipelineData::byte_stream -> create a PipelineData::ByteStream
And use these functions everywhere.

### Reason behind this change
I tried to implement `pipefail` feature, but this would required to
change `PipelineData` from enum to struct. So use these functions can
reduce diff if I finally change to struct. [Discord message
here](https://discord.com/channels/601130461678272522/615962413203718156/1396999539000479784)
is my plan.

# User-Facing Changes
NaN

# Tests + Formatting
NaN

# After Submitting
NaN
2025-08-02 09:30:30 +08:00
ee5b5bd39e fix(input list): don't leak ansi styling, fuzzy match indicator preserves styles (#16276)
- fixes #16200

# Description

|    | Select           | Fuzzy           |
| -- | ---------------- | --------------- |
|  | ![select-before] | ![fuzzy-before] |
|  | ![select-fixed]  | ![fuzzy-fixed]  |

[select-before]:
8fe9136472/select-before.svg
[select-fixed]:
8fe9136472/select-after.svg
[fuzzy-before]:
8fe9136472/fuzzy-before.svg
[fuzzy-fixed]:
8fe9136472/fuzzy-after.svg

Using a custom `dialoguer::theme::Theme` implementation, how `input
list` renders items are overridden.

Unfortunately, implementing one of the methods requires
`fuzzy_matcher::skim::SkimMatcherV2` which `dialoguer` does not export
by itself.
Had to add an explicit dependency to `fuzzy_matcher`, which we already
depend on through `dialoguer`. Version specification is copied from
`dialoguer`.

# Tests + Formatting
No tests added.
Couldn't find existing tests, not sure how to test this.

---------

Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
2025-07-31 22:42:10 +02:00
d565c9ed01 Fix commit ID hex formatting in gstat (#16309)
Fix commit ID hex formatting in gstat, closes #16307 

Leading zeros are preserved, maintaining the correct hex representation

This issue is relatively easy to fix, but it's not very easy to verify.
However, I have already tested several scenarios, it works for commit
sha like `000baeef`, `000003c7` and `00000002` etc.
2025-07-31 09:59:14 -05:00
18d5d8aae1 Fixup pre-release checkup workflow (#16305)
Avoid going out of disk by running cargo clean between each build:
https://github.com/taiki-e/cargo-hack#--clean-per-run

Also rename to something shorter for the overview
2025-07-30 23:46:18 +02:00
89c0e325fa fix panic when ..= syntax is used in stepped ranges (#16231)
Fixes #16185

# Description

Stepped range literals where `..=` precedes the second value no longer
cause a parser panic:

```diff
random int 1..=3..5
-Error:   x Main thread panicked.
-  |-> at crates/nu-protocol/src/engine/state_working_set.rs:400:48
-  `-> slice index starts at 8 but ends at 7
+Error: nu::parser::parse_mismatch
+
+  × Parse mismatch during operation.
+   ╭─[entry #1:1:15]
+ 1 │ random int 1..=3..5
+   ·               ─┬
+   ·                ╰── expected number
```
2025-07-30 23:38:59 +02:00
7f2beb49db Manual GH workflow for cargo hack before release (#16304)
Currently we run this locally on whatever machine the person doing it is
using. With this we have all three major platforms covered and don't
block a personal machine before the release. Also any failures are
visible to everyone if we need to fix something.
2025-07-30 23:05:33 +02:00
7203138880 Bump version to 0.106.2 (#16295) 2025-07-30 01:36:35 +02:00
459f3c0c28 feat(watch): implement --debounce flag with duration (#16187)
- fixes #16178
- `watch --debounce-ms` deprecated

Co-authored-by: Luca Scherzer <luca.scherzer@de.clara.net>
2025-07-29 17:10:40 +03:00
2e4900f085 Check type of row conditions at parse-time (#16175)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

As a bonus to #16174, I realized it would be trivial to add a similar
check to where.

Before:
```nushell
1..100 | where 1
# => no output...
```

After:
```nushell
1..100 | where 1
# => Error: nu::parser::type_mismatch
# => 
# =>   × Type mismatch.
# =>    ╭─[entry #3:1:16]
# =>  1 │ 1..100 | where 1
# =>    ·                ┬
# =>    ·                ╰── expected bool, found int
# =>    ╰────
```

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
* `where` should now error on row condition expressions which are not
booleans
  
# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->
Added test

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
N/A
2025-07-28 22:24:12 -04:00
c921eadc6a Don't import IoError on nu-plugin-core without local-socket (#16279)
When you use `nu-plugin-core` without the `local-socket` feature, you
got the warning:
<img width="1182" height="353" alt="image"
src="https://github.com/user-attachments/assets/dd80af11-4963-4d48-8c93-43e6c2dabafa"
/>

This PR fixes that warning.
2025-07-28 20:10:17 +02:00
00ac34d716 Port unsafe_op_in_unsafe_fn fix to FreeBSD (#16275)
Same general idea as https://github.com/nushell/nushell/pull/16266

Fixes the 2024 edition
[`unsafe_op_in_unsafe_fn`](https://doc.rust-lang.org/nightly/edition-guide/rust-2024/unsafe-op-in-unsafe-fn.html)
lint for FreeBSD as well

Add safety comments to both implementations and an assertion before
`MaybeUninit::assume_init`
2025-07-28 08:42:23 +02:00
pin
28a796d5cb Fix #16261 (#16266)
- this PR should close #16261 
- fixes #16261

Confirmed to build with Rust-1.86 and to yield a working binary.
2025-07-27 21:27:35 +02:00
f8698a6c24 fix(get): run_const uses --optional flag (#16268)
`Get::run_const()` was not update along `Get::run()` in #16007.

Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
2025-07-27 18:17:33 +03:00
48bca0a058 Reapply "refactor(completion, parser): move custom_completion info from Expression to Signature" (#16250) (#16259)
This reverts commit aeb517867e. The
Nushell version has bumped, so it's okay to reapply the changes from
https://github.com/nushell/nushell/pull/15613.
2025-07-25 19:57:00 -04:00
f3d92e3fa1 fix bare interpolation regression (#16235)
Regression from #16204 

Before:

![](f1995bc71f/before.svg)

After:

![](f1995bc71f/after.svg)

# Tests + Formatting
+1

---------

Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
2025-07-25 08:19:15 +03:00
57dce8a386 Bump patch version (#16236)
Bump patch version
2025-07-25 03:28:24 +08:00
aeb517867e Revert "refactor(completion, parser): move custom_completion info from Expression to Signature" (#16250)
Reverts nushell/nushell#15613 because we haven't bumped to the 106.1 dev
version yet
2025-07-24 15:10:47 -04:00
71baeff287 refactor(completion, parser): move custom_completion info from Expression to Signature (#15613)
Restricts custom completion from universal to internal arguments only.

Pros:
1. Less memory
2. More flexible for later customizations, e.g. #14923 

Cons:
1. limited customization capabilities, but at least covers all currently
existing features in nushell.

# Description

Mostly vibe coded by [Zed AI](https://zed.dev/ai) with a single prompt.
LGTM, but I'm not so sure @ysthakur 

# User-Facing Changes

Hopefully none.

# Tests + Formatting

+3

# After Submitting

---------

Co-authored-by: Yash Thakur <45539777+ysthakur@users.noreply.github.com>
2025-07-24 14:21:58 -04:00
280 changed files with 3983 additions and 2083 deletions

View File

@ -1,40 +1,16 @@
<!--
if this PR closes one or more issues, you can automatically link the PR with
them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g.
- this PR should close #xxxx
- fixes #xxxx
Thank you for improving Nushell!
Please, read our contributing guide: https://github.com/nushell/nushell/blob/main/CONTRIBUTING.md
you can also mention related issues, PRs or discussions!
Use the following space to include the motivation and any technical details behind this PR.
-->
# Description
## Release notes summary - What our users need to know
<!--
Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes.
Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience.
This section will be included as part of our release notes. See the contributing guide for more details.
If you're not confident about this, a core team member would be glad to help!
-->
# User-Facing Changes
<!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. -->
# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.
Make sure you've run and fixed any issues with these commands:
- `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library
> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it automatically
> toolkit check pr
> ```
-->
# After Submitting
<!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
## Tasks after submitting
<!-- Remove any tasks which aren't relevant for your PR, or add your own -->
- [ ] Update the [documentation](https://github.com/nushell/nushell.github.io)

View File

@ -0,0 +1,44 @@
name: Checks to perform pre-release (manual)
on:
- workflow_dispatch
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:
strategy:
fail-fast: true
matrix:
platform: [windows-latest, macos-latest, ubuntu-22.04]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@cargo-hack
- name: Feature power set
run: |
cargo hack --all --feature-powerset --at-least-one-of rustls-tls,native-tls --mutually-exclusive-features rustls-tls,native-tls --mutually-exclusive-features rustls-tls,static-link-openssl --skip default-no-clipboard,stable,mimalloc check
# Don't build fully for now as it will run out of disk space
# - name: Build all crates
# run: cargo hack --all build --clean-per-run
- name: Check for clean repo
shell: bash
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "there are changes";
git status --porcelain
exit 1
else
echo "no changes in working directory";
fi

View File

@ -10,4 +10,4 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Check spelling
uses: crate-ci/typos@v1.34.0
uses: crate-ci/typos@v1.35.4

View File

@ -3,6 +3,7 @@
Welcome to Nushell and thank you for considering contributing!
## Table of contents
- [Tips for submitting PRs](#tips-for-submitting-prs)
- [Proposing design changes](#proposing-design-changes)
- [Developing](#developing)
- [Setup](#setup)
@ -20,6 +21,51 @@ More resources can be found in the nascent [developer documentation](devdocs/REA
- [Platform support policy](devdocs/PLATFORM_SUPPORT.md)
- [Our Rust style](devdocs/rust_style.md)
## Tips for submitting PRs
Thank you for improving Nushell! We are always glad to see contributions, and we are absolutely willing to talk through the design or implementation of your PR. Come talk with us in [Discord](https://discordapp.com/invite/NtAbbGn), or create a GitHub discussion or draft PR and we can help you work out the details from there.
**Please talk to the core team before making major changes!** See the [proposing design changes](#proposing-design-changes) for more details.
### Release notes section
In our PR template, we have a "Release notes summary" section which will be included in our release notes for our blog.
This section should include all information about your change which is relevant to a user of Nushell. You should try to keep it **brief and simple to understand**, and focus on the ways your change directly impacts the user experience. We highly encourage adding examples and, when relevant, screenshots in this section.
Please make sure to consider both the *intended changes*, such as additions or deliberate breaking changes **and** possible *side effects* that might change how users interact with a command or feature. It's important to think carefully about the ways that your PR might affect any aspect of the user experience, and to document these changes even if they seem minor or aren't directly related to the main purpose of the PR.
This section might not be relevant for all PRs. If your PR is a work in progress, feel free to write "WIP"/"TODO"/etc in this section. You can also write "N/A" if this is a technical change which doesn't impact the user experience.
If you're not sure what to put here, or need some help, **a core team member would be glad to help you out**. We may also makes some tweaks to your release notes section. Please don't take it personally, we just want to make sure our release notes are polished and easy to understand. Once the release notes section is ready, we'll add the (TODO label name) label to indicate that the release notes section is ready to be included in the actual release notes.
### Tests and formatting checks
Our CI system automatically checks formatting and runs our tests. If you're running into an issue, or just want to make sure everything is ready to go before creating your PR, you can run the checks yourself:
```nushell
use toolkit.nu # or use an `env_change` hook to activate it automatically
toolkit check pr
```
Furthermore, you can also runs these checks individually with the subcommands of `toolkit`, or run the underlying commands yourself:
- `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make sure to enable [developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library
If the checks are passing on your local system, but CI just won't pass, feel free to ask for help from the core team.
### Linking and mentioning issues
If your PR closes one or more issues, you can automatically link the PR with them by using one of the [linking keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword):
- This PR should close #xxxx
- Fixes #xxxx
You can also mention related issues, PRs or discussions!
## 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.

336
Cargo.lock generated
View File

@ -483,7 +483,7 @@ dependencies = [
"hex",
"hmac",
"http 0.2.12",
"http 1.2.0",
"http 1.3.1",
"once_cell",
"percent-encoding",
"sha2",
@ -578,7 +578,7 @@ dependencies = [
"aws-smithy-types",
"bytes",
"http 0.2.12",
"http 1.2.0",
"http 1.3.1",
"pin-project-lite",
"tokio",
"tracing",
@ -595,7 +595,7 @@ dependencies = [
"bytes",
"bytes-utils",
"http 0.2.12",
"http 1.2.0",
"http 1.3.1",
"http-body 0.4.6",
"http-body 1.0.1",
"http-body-util",
@ -922,6 +922,12 @@ dependencies = [
"shlex",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
@ -1111,6 +1117,16 @@ dependencies = [
"supports-color",
]
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "comfy-table"
version = "7.1.3"
@ -1879,9 +1895,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy-regex"
version = "0.14.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
checksum = "bf04c5ec15464ace8355a7b440a33aece288993475556d461154d7a62ad9947c"
dependencies = [
"bit-set",
"regex-automata",
@ -2279,7 +2295,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
"http 1.2.0",
"http 1.3.1",
"indexmap",
"slab",
"tokio",
@ -2421,9 +2437,9 @@ dependencies = [
[[package]]
name = "http"
version = "1.2.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
@ -2448,7 +2464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.2.0",
"http 1.3.1",
]
[[package]]
@ -2459,7 +2475,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http 1.2.0",
"http 1.3.1",
"http-body 1.0.1",
"pin-project-lite",
]
@ -2529,7 +2545,7 @@ dependencies = [
"futures-channel",
"futures-util",
"h2 0.4.7",
"http 1.2.0",
"http 1.3.1",
"http-body 1.0.1",
"httparse",
"httpdate",
@ -2563,10 +2579,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.2.0",
"http 1.3.1",
"hyper 1.5.1",
"hyper-util",
"rustls 0.23.20",
"rustls 0.23.28",
"rustls-native-certs 0.8.1",
"rustls-pki-types",
"tokio",
@ -2600,7 +2616,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.2.0",
"http 1.3.1",
"http-body 1.0.1",
"hyper 1.5.1",
"pin-project-lite",
@ -2954,6 +2970,28 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.32"
@ -3185,9 +3223,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.22"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lru"
@ -3271,9 +3309,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mach2"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
@ -3437,7 +3475,7 @@ dependencies = [
"assert-json-diff",
"bytes",
"futures-util",
"http 1.2.0",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.1",
@ -3583,7 +3621,7 @@ dependencies = [
[[package]]
name = "nu"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"assert_cmd",
"crossterm",
@ -3638,7 +3676,7 @@ dependencies = [
[[package]]
name = "nu-cli"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"chrono",
"crossterm",
@ -3675,7 +3713,7 @@ dependencies = [
[[package]]
name = "nu-cmd-base"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"indexmap",
"miette",
@ -3687,7 +3725,7 @@ dependencies = [
[[package]]
name = "nu-cmd-extra"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"fancy-regex",
"heck",
@ -3713,7 +3751,7 @@ dependencies = [
[[package]]
name = "nu-cmd-lang"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"itertools 0.14.0",
"miette",
@ -3730,7 +3768,7 @@ dependencies = [
[[package]]
name = "nu-cmd-plugin"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"itertools 0.14.0",
"nu-engine",
@ -3741,7 +3779,7 @@ dependencies = [
[[package]]
name = "nu-color-config"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"nu-ansi-term",
"nu-engine",
@ -3753,7 +3791,7 @@ dependencies = [
[[package]]
name = "nu-command"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"alphanumeric-sort",
"base64 0.22.1",
@ -3778,7 +3816,9 @@ dependencies = [
"fancy-regex",
"filesize",
"filetime",
"fuzzy-matcher",
"getrandom 0.2.15",
"http 1.3.1",
"human-date-parser",
"indexmap",
"indicatif",
@ -3831,7 +3871,7 @@ dependencies = [
"rstest",
"rstest_reuse",
"rusqlite",
"rustls 0.23.20",
"rustls 0.23.28",
"rustls-native-certs 0.8.1",
"scopeguard",
"serde",
@ -3849,7 +3889,7 @@ dependencies = [
"unicode-segmentation",
"unicode-width 0.2.0",
"update-informer",
"ureq 2.12.1",
"ureq",
"url",
"uu_cp",
"uu_mkdir",
@ -3871,7 +3911,7 @@ dependencies = [
[[package]]
name = "nu-derive-value"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"heck",
"proc-macro-error2",
@ -3882,7 +3922,7 @@ dependencies = [
[[package]]
name = "nu-engine"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"fancy-regex",
"log",
@ -3894,7 +3934,7 @@ dependencies = [
[[package]]
name = "nu-experimental"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"itertools 0.14.0",
"thiserror 2.0.12",
@ -3902,7 +3942,7 @@ dependencies = [
[[package]]
name = "nu-explore"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"ansi-str",
"anyhow",
@ -3926,14 +3966,14 @@ dependencies = [
[[package]]
name = "nu-glob"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"doc-comment",
]
[[package]]
name = "nu-json"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"fancy-regex",
"linked-hash-map",
@ -3949,7 +3989,7 @@ dependencies = [
[[package]]
name = "nu-lsp"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"assert-json-diff",
"crossbeam-channel",
@ -3976,7 +4016,7 @@ dependencies = [
[[package]]
name = "nu-parser"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"bytesize",
"chrono",
@ -3993,7 +4033,7 @@ dependencies = [
[[package]]
name = "nu-path"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"dirs",
"omnipath",
@ -4003,7 +4043,7 @@ dependencies = [
[[package]]
name = "nu-plugin"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"log",
"nix 0.29.0",
@ -4019,7 +4059,7 @@ dependencies = [
[[package]]
name = "nu-plugin-core"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"interprocess",
"log",
@ -4033,7 +4073,7 @@ dependencies = [
[[package]]
name = "nu-plugin-engine"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"log",
"nu-engine",
@ -4049,7 +4089,7 @@ dependencies = [
[[package]]
name = "nu-plugin-protocol"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"nu-protocol",
"nu-utils",
@ -4061,7 +4101,7 @@ dependencies = [
[[package]]
name = "nu-plugin-test-support"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"nu-ansi-term",
"nu-cmd-lang",
@ -4079,7 +4119,7 @@ dependencies = [
[[package]]
name = "nu-pretty-hex"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"heapless",
"nu-ansi-term",
@ -4088,7 +4128,7 @@ dependencies = [
[[package]]
name = "nu-protocol"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"brotli",
"bytes",
@ -4130,7 +4170,7 @@ dependencies = [
[[package]]
name = "nu-std"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"log",
"miette",
@ -4141,7 +4181,7 @@ dependencies = [
[[package]]
name = "nu-system"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"chrono",
"itertools 0.14.0",
@ -4159,7 +4199,7 @@ dependencies = [
[[package]]
name = "nu-table"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"fancy-regex",
"nu-ansi-term",
@ -4172,7 +4212,7 @@ dependencies = [
[[package]]
name = "nu-term-grid"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"nu-utils",
"unicode-width 0.2.0",
@ -4180,7 +4220,7 @@ dependencies = [
[[package]]
name = "nu-test-support"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"nu-glob",
"nu-path",
@ -4192,7 +4232,7 @@ dependencies = [
[[package]]
name = "nu-utils"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"crossterm",
"crossterm_winapi",
@ -4221,7 +4261,7 @@ dependencies = [
[[package]]
name = "nu_plugin_example"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"nu-cmd-lang",
"nu-plugin",
@ -4231,7 +4271,7 @@ dependencies = [
[[package]]
name = "nu_plugin_formats"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"chrono",
"eml-parser",
@ -4246,7 +4286,7 @@ dependencies = [
[[package]]
name = "nu_plugin_gstat"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"git2",
"nu-plugin",
@ -4255,7 +4295,7 @@ dependencies = [
[[package]]
name = "nu_plugin_inc"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"nu-plugin",
"nu-protocol",
@ -4264,7 +4304,7 @@ dependencies = [
[[package]]
name = "nu_plugin_polars"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"aws-config",
"aws-credential-types",
@ -4303,7 +4343,7 @@ dependencies = [
[[package]]
name = "nu_plugin_query"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"gjson",
"nu-plugin",
@ -4318,7 +4358,7 @@ dependencies = [
[[package]]
name = "nu_plugin_stress_internals"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"interprocess",
"serde",
@ -4442,7 +4482,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nuon"
version = "0.106.0"
version = "0.106.2"
dependencies = [
"chrono",
"nu-engine",
@ -4590,7 +4630,7 @@ dependencies = [
"chrono",
"form_urlencoded",
"futures",
"http 1.2.0",
"http 1.3.1",
"http-body-util",
"humantime",
"hyper 1.5.1",
@ -5819,7 +5859,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.0",
"rustls 0.23.20",
"rustls 0.23.28",
"socket2",
"thiserror 2.0.12",
"tokio",
@ -5837,7 +5877,7 @@ dependencies = [
"rand 0.8.5",
"ring",
"rustc-hash 2.1.0",
"rustls 0.23.20",
"rustls 0.23.28",
"rustls-pki-types",
"slab",
"thiserror 2.0.12",
@ -5977,9 +6017,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
@ -5987,9 +6027,9 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.12.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
@ -6044,8 +6084,7 @@ dependencies = [
[[package]]
name = "reedline"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b627c435d0189363b15f885f1b07193d310ec9e4e39c5627951c6e0f4d02c93a"
source = "git+https://github.com/nushell/reedline?branch=main#faee143a688846d98e260407b4e09f653eb31307"
dependencies = [
"arboard",
"chrono",
@ -6137,7 +6176,7 @@ dependencies = [
"futures-core",
"futures-util",
"h2 0.4.7",
"http 1.2.0",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.1",
@ -6153,7 +6192,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.20",
"rustls 0.23.28",
"rustls-native-certs 0.8.1",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
@ -6412,15 +6451,15 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.20"
version = "0.23.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"rustls-webpki 0.103.3",
"subtle",
"zeroize",
]
@ -6469,13 +6508,41 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.10.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
dependencies = [
"core-foundation 0.10.0",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls 0.23.28",
"rustls-native-certs 0.8.1",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.3",
"security-framework 3.0.1",
"security-framework-sys",
"webpki-root-certs 0.26.11",
"windows-sys 0.59.0",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@ -6488,9 +6555,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.102.8"
version = "0.103.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
dependencies = [
"ring",
"rustls-pki-types",
@ -7173,9 +7240,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.36.0"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aab138f5c1bb35231de19049060a87977ad23e04f2303e953bc5c2947ac7dec4"
checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d"
dependencies = [
"libc",
"memchr",
@ -7468,7 +7535,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls 0.23.20",
"rustls 0.23.28",
"tokio",
]
@ -7777,49 +7844,30 @@ dependencies = [
"semver",
"serde",
"serde_json",
"ureq 3.0.3",
"ureq",
]
[[package]]
name = "ureq"
version = "2.12.1"
version = "3.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
checksum = "9f0fde9bc91026e381155f8c67cb354bcd35260b2f4a29bcc84639f762760c39"
dependencies = [
"base64 0.22.1",
"cookie_store",
"der",
"encoding_rs",
"flate2",
"log",
"native-tls",
"once_cell",
"rustls 0.23.20",
"percent-encoding",
"rustls 0.23.28",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"socks",
"url",
"webpki-roots 0.26.8",
]
[[package]]
name = "ureq"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "217751151c53226090391713e533d9a5e904ba2570dabaaace29032687589c3e"
dependencies = [
"base64 0.22.1",
"cc",
"cookie_store",
"der",
"flate2",
"log",
"native-tls",
"percent-encoding",
"rustls 0.23.20",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"serde",
"serde_json",
"ureq-proto",
"utf-8",
"webpki-root-certs 0.26.11",
@ -7828,12 +7876,12 @@ dependencies = [
[[package]]
name = "ureq-proto"
version = "0.3.5"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae239d0a3341aebc94259414d1dc67cfce87d41cbebc816772c91b77902fafa4"
checksum = "59db78ad1923f2b1be62b6da81fe80b173605ca0d57f85da2e005382adf693f7"
dependencies = [
"base64 0.22.1",
"http 1.2.0",
"http 1.3.1",
"httparse",
"log",
]
@ -8605,6 +8653,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -8632,6 +8689,21 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@ -8672,6 +8744,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@ -8684,6 +8762,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@ -8696,6 +8780,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@ -8714,6 +8804,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@ -8726,6 +8822,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@ -8738,6 +8840,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@ -8750,6 +8858,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"

View File

@ -10,8 +10,8 @@ homepage = "https://www.nushell.sh"
license = "MIT"
name = "nu"
repository = "https://github.com/nushell/nushell"
rust-version = "1.86.0"
version = "0.106.0"
rust-version = "1.87.0"
version = "0.106.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -83,15 +83,17 @@ csv = "1.3"
ctrlc = "3.4"
devicons = "0.6.12"
dialoguer = { default-features = false, version = "0.11" }
fuzzy-matcher = { version = "^0.3.7" }
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"
fancy-regex = "0.16"
filesize = "0.2"
filetime = "0.2"
heck = "0.5.0"
http = "1.3.1"
human-date-parser = "0.3.0"
indexmap = "2.10"
indicatif = "0.17"
@ -140,7 +142,7 @@ rand = "0.9"
getrandom = "0.2" # pick same version that rand requires
rand_chacha = "0.9"
ratatui = "0.29"
rayon = "1.10"
rayon = "1.11"
reedline = "0.41.0"
rmp = "0.8"
rmp-serde = "1.3"
@ -149,7 +151,10 @@ 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"] }
# We have to fix rustls and ureq versions
# because we use unversioned api to allow users set up their own
# crypto providers (grep for "unversioned")
rustls = { version = "=0.23.28", default-features = false, features = ["std", "tls12"] }
rustls-native-certs = "0.8"
scopeguard = { version = "1.2.0" }
serde = { version = "1.0" }
@ -172,7 +177,7 @@ update-informer = { version = "1.3.0", default-features = false, features = ["gi
umask = "2.1"
unicode-segmentation = "1.12"
unicode-width = "0.2"
ureq = { version = "2.12", default-features = false, features = ["socks-proxy"] }
ureq = { version = "=3.0.12", default-features = false, features = ["socks-proxy"] }
url = "2.2"
uu_cp = "0.0.30"
uu_mkdir = "0.0.30"
@ -198,28 +203,29 @@ webpki-roots = "1.0"
# todo = "warn"
unchecked_duration_subtraction = "warn"
used_underscore_binding = "warn"
result_large_err = "allow"
[lints]
workspace = true
[dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.106.0" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.106.0" }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.106.0" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.106.0" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.106.0", optional = true }
nu-command = { path = "./crates/nu-command", version = "0.106.0", default-features = false, features = ["os"] }
nu-engine = { path = "./crates/nu-engine", version = "0.106.0" }
nu-experimental = { path = "./crates/nu-experimental", version = "0.106.0" }
nu-explore = { path = "./crates/nu-explore", version = "0.106.0" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.106.0" }
nu-parser = { path = "./crates/nu-parser", version = "0.106.0" }
nu-path = { path = "./crates/nu-path", version = "0.106.0" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.106.0" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.106.0" }
nu-std = { path = "./crates/nu-std", version = "0.106.0" }
nu-system = { path = "./crates/nu-system", version = "0.106.0" }
nu-utils = { path = "./crates/nu-utils", version = "0.106.0" }
nu-cli = { path = "./crates/nu-cli", version = "0.106.2" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.106.2" }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.106.2" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.106.2" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.106.2", optional = true }
nu-command = { path = "./crates/nu-command", version = "0.106.2", default-features = false, features = ["os"] }
nu-engine = { path = "./crates/nu-engine", version = "0.106.2" }
nu-experimental = { path = "./crates/nu-experimental", version = "0.106.2" }
nu-explore = { path = "./crates/nu-explore", version = "0.106.2" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.106.2" }
nu-parser = { path = "./crates/nu-parser", version = "0.106.2" }
nu-path = { path = "./crates/nu-path", version = "0.106.2" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.106.2" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.106.2" }
nu-std = { path = "./crates/nu-std", version = "0.106.2" }
nu-system = { path = "./crates/nu-system", version = "0.106.2" }
nu-utils = { path = "./crates/nu-utils", version = "0.106.2" }
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
crossterm = { workspace = true }
@ -248,9 +254,9 @@ nix = { workspace = true, default-features = false, features = [
] }
[dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", version = "0.106.0" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.106.0" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.106.0" }
nu-test-support = { path = "./crates/nu-test-support", version = "0.106.2" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.106.2" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.106.2" }
assert_cmd = "2.0"
dirs = { workspace = true }
tango-bench = "0.6"
@ -337,7 +343,7 @@ 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" }
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`

View File

@ -1,6 +1,6 @@
# 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.
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.
@ -11,7 +11,7 @@ Only if you provide a strong reasoning and the necessary resources, will we cons
## 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.
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?

View File

@ -5,29 +5,29 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
edition = "2024"
license = "MIT"
name = "nu-cli"
version = "0.106.0"
version = "0.106.2"
[lib]
bench = false
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.106.0" }
nu-command = { path = "../nu-command", version = "0.106.0" }
nu-std = { path = "../nu-std", version = "0.106.0" }
nu-test-support = { path = "../nu-test-support", version = "0.106.0" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.106.2" }
nu-command = { path = "../nu-command", version = "0.106.2" }
nu-std = { path = "../nu-std", version = "0.106.2" }
nu-test-support = { path = "../nu-test-support", version = "0.106.2" }
rstest = { workspace = true, default-features = false }
tempfile = { workspace = true }
[dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.106.0" }
nu-engine = { path = "../nu-engine", version = "0.106.0", features = ["os"] }
nu-glob = { path = "../nu-glob", version = "0.106.0" }
nu-path = { path = "../nu-path", version = "0.106.0" }
nu-parser = { path = "../nu-parser", version = "0.106.0" }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.106.0", optional = true }
nu-protocol = { path = "../nu-protocol", version = "0.106.0", features = ["os"] }
nu-utils = { path = "../nu-utils", version = "0.106.0" }
nu-color-config = { path = "../nu-color-config", version = "0.106.0" }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.106.2" }
nu-engine = { path = "../nu-engine", version = "0.106.2", features = ["os"] }
nu-glob = { path = "../nu-glob", version = "0.106.2" }
nu-path = { path = "../nu-path", version = "0.106.2" }
nu-parser = { path = "../nu-parser", version = "0.106.2" }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.106.2", optional = true }
nu-protocol = { path = "../nu-protocol", version = "0.106.2", features = ["os"] }
nu-utils = { path = "../nu-utils", version = "0.106.2" }
nu-color-config = { path = "../nu-color-config", version = "0.106.2" }
nu-ansi-term = { workspace = true }
reedline = { workspace = true, features = ["bashisms", "sqlite"] }

View File

@ -348,8 +348,43 @@ impl NuCompleter {
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) {
// Get custom completion from PositionalArg or Flag
let custom_completion_decl_id = {
// Check PositionalArg or Flag from Signature
let signature = working_set.get_decl(call.decl_id).signature();
match arg {
// For named arguments, check Flag
Argument::Named((name, short, value)) => {
if value.as_ref().is_none_or(|e| !e.span.contains(pos)) {
None
} else {
// If we're completing the value of the flag,
// search for the matching custom completion decl_id (long or short)
let flag =
signature.get_long_flag(&name.item).or_else(|| {
short.as_ref().and_then(|s| {
signature.get_short_flag(
s.item.chars().next().unwrap_or('_'),
)
})
});
flag.and_then(|f| f.custom_completion)
}
}
// For positional arguments, check PositionalArg
Argument::Positional(_) => {
// Find the right positional argument by index
let arg_pos = positional_arg_indices.len();
signature
.get_positional(arg_pos)
.and_then(|pos_arg| pos_arg.custom_completion)
}
_ => None,
}
};
if let Some(decl_id) = custom_completion_decl_id {
// for `--foo <tab>` and `--foo=<tab>`, the arg span should be trimmed
let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
strip_placeholder_with_rsplit(
@ -718,7 +753,7 @@ impl NuCompleter {
Ok(value) => {
log::error!(
"External completer returned invalid value of type {}",
value.get_type().to_string()
value.get_type()
);
Some(vec![])
}

View File

@ -140,7 +140,7 @@ impl<T: Completer> Completer for CustomCompletion<T> {
_ => {
log::error!(
"Custom completer returned invalid value of type {}",
value.get_type().to_string()
value.get_type()
);
return vec![];
}

View File

@ -278,7 +278,7 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool {
&mut stack,
&old_contents,
&old_plugin_file_path.to_string_lossy(),
PipelineData::Empty,
PipelineData::empty(),
false,
) != 0
{

View File

@ -61,7 +61,7 @@ fn get_prompt_string(
.and_then(|v| match v {
Value::Closure { val, .. } => {
let result = ClosureEvalOnce::new(engine_state, stack, val.as_ref().clone())
.run_with_input(PipelineData::Empty);
.run_with_input(PipelineData::empty());
trace!(
"get_prompt_string (block) {}:{}:{}",
@ -76,7 +76,7 @@ fn get_prompt_string(
})
.ok()
}
Value::String { .. } => Some(PipelineData::Value(v.clone(), None)),
Value::String { .. } => Some(PipelineData::value(v.clone(), None)),
_ => None,
})
.and_then(|pipeline_data| {

View File

@ -159,7 +159,7 @@ pub(crate) fn add_menus(
engine_state.merge_delta(delta)?;
let mut temp_stack = Stack::new().collect_value();
let input = PipelineData::Empty;
let input = PipelineData::empty();
menu_eval_results.push(eval_block::<WithoutDebug>(
&engine_state,
&mut temp_stack,
@ -1047,6 +1047,10 @@ fn event_from_record(
ReedlineEvent::ExecuteHostCommand(cmd.to_expanded_string("", config))
}
"openeditor" => ReedlineEvent::OpenEditor,
"vichangemode" => {
let mode = extract_value("mode", record, span)?;
ReedlineEvent::ViChangeMode(mode.as_str()?.to_owned())
}
str => {
return Err(ShellError::InvalidValue {
valid: "a reedline event".into(),

View File

@ -325,7 +325,19 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
perf!("reset signals", start_time, use_color);
start_time = std::time::Instant::now();
// Right before we start our prompt and take input from the user, fire the "pre_prompt" hook
// Check all the environment variables they ask for
// fire the "env_change" hook
if let Err(error) = hook::eval_env_change_hook(
&engine_state.get_config().hooks.env_change.clone(),
engine_state,
&mut stack,
) {
report_shell_error(engine_state, &error)
}
perf!("env-change hook", start_time, use_color);
start_time = std::time::Instant::now();
// Next, right before we start our prompt and take input from the user, fire the "pre_prompt" hook
if let Err(err) = hook::eval_hooks(
engine_state,
&mut stack,
@ -337,18 +349,6 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
}
perf!("pre-prompt hook", start_time, use_color);
start_time = std::time::Instant::now();
// Next, check all the environment variables they ask for
// fire the "env_change" hook
if let Err(error) = hook::eval_env_change_hook(
&engine_state.get_config().hooks.env_change.clone(),
engine_state,
&mut stack,
) {
report_shell_error(engine_state, &error)
}
perf!("env-change hook", start_time, use_color);
let engine_reference = Arc::new(engine_state.clone());
let config = stack.get_config(engine_state);

View File

@ -97,8 +97,11 @@ fn extern_completer() -> NuCompleter {
// Add record value as example
let record = r#"
def animals [] { [ "cat", "dog", "eel" ] }
def fruits [] { [ "apple", "banana" ] }
extern spam [
animal: string@animals
fruit?: string@fruits
...rest: string@animals
--foo (-f): string@animals
-b: string@animals
]
@ -1560,9 +1563,7 @@ fn flag_completions() {
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
// Test completions for the 'ls' flags
let suggestions = completer.complete("ls -", 4);
assert_eq!(18, suggestions.len());
let expected: Vec<_> = vec![
"--all",
"--directory",
@ -1583,9 +1584,12 @@ fn flag_completions() {
"-s",
"-t",
];
// Match results
match_suggestions(&expected, &suggestions);
// https://github.com/nushell/nushell/issues/16375
let suggestions = completer.complete("table -", 7);
assert_eq!(20, suggestions.len());
}
#[test]
@ -2137,7 +2141,8 @@ fn run_external_completion_within_pwd(
assert!(engine_state.merge_delta(delta).is_ok());
assert!(
eval_block::<WithoutDebug>(&engine_state, &mut stack, &block, PipelineData::Empty).is_ok()
eval_block::<WithoutDebug>(&engine_state, &mut stack, &block, PipelineData::empty())
.is_ok()
);
// Merge environment into the permanent state
@ -2261,6 +2266,22 @@ fn extern_custom_completion_positional(mut extern_completer: NuCompleter) {
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_optional(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam foo -f bar ", 16);
let expected: Vec<_> = vec!["apple", "banana"];
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_rest(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam foo -f bar baz ", 20);
let expected: Vec<_> = vec!["cat", "dog", "eel"];
match_suggestions(&expected, &suggestions);
let suggestions = extern_completer.complete("spam foo -f bar baz qux ", 24);
match_suggestions(&expected, &suggestions);
}
#[rstest]
fn extern_custom_completion_long_flag_1(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam --foo=", 11);
@ -2289,6 +2310,17 @@ fn extern_custom_completion_short_flag(mut extern_completer: NuCompleter) {
match_suggestions(&expected, &suggestions);
}
/// When we're completing the flag name itself, not its value,
/// custom completions should not be used
#[rstest]
fn custom_completion_flag_name_not_value(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam --f", 8);
match_suggestions(&vec!["--foo"], &suggestions);
// Also test with partial short flag
let suggestions = extern_completer.complete("spam -f", 7);
match_suggestions(&vec!["-f"], &suggestions);
}
#[rstest]
fn extern_complete_flags(mut extern_completer: NuCompleter) {
let suggestions = extern_completer.complete("spam -", 6);

View File

@ -199,7 +199,7 @@ pub fn merge_input(
engine_state,
stack,
&block,
PipelineData::Value(Value::nothing(Span::unknown()), None),
PipelineData::value(Value::nothing(Span::unknown()), None),
)
.is_ok()
);

View File

@ -5,7 +5,7 @@ edition = "2024"
license = "MIT"
name = "nu-cmd-base"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
version = "0.106.0"
version = "0.106.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,10 +13,10 @@ version = "0.106.0"
workspace = true
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.106.0", default-features = false }
nu-parser = { path = "../nu-parser", version = "0.106.0" }
nu-path = { path = "../nu-path", version = "0.106.0" }
nu-protocol = { path = "../nu-protocol", version = "0.106.0", default-features = false }
nu-engine = { path = "../nu-engine", version = "0.106.2", default-features = false }
nu-parser = { path = "../nu-parser", version = "0.106.2" }
nu-path = { path = "../nu-path", version = "0.106.2" }
nu-protocol = { path = "../nu-protocol", version = "0.106.2", default-features = false }
indexmap = { workspace = true }
miette = { workspace = true }

View File

@ -12,7 +12,7 @@ use nu_protocol::{
/// ```rust
/// # use nu_engine::command_prelude::*;
/// # use nu_cmd_base::WrapCall;
/// # fn do_command_logic(call: WrapCall) -> Result<PipelineData, ShellError> { Ok(PipelineData::Empty) }
/// # fn do_command_logic(call: WrapCall) -> Result<PipelineData, ShellError> { Ok(PipelineData::empty()) }
///
/// # struct Command {}
/// # impl Command {

View File

@ -5,7 +5,7 @@ edition = "2024"
license = "MIT"
name = "nu-cmd-extra"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
version = "0.106.0"
version = "0.106.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -16,13 +16,13 @@ bench = false
workspace = true
[dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.106.0" }
nu-engine = { path = "../nu-engine", version = "0.106.0", default-features = false }
nu-json = { version = "0.106.0", path = "../nu-json" }
nu-parser = { path = "../nu-parser", version = "0.106.0" }
nu-pretty-hex = { version = "0.106.0", path = "../nu-pretty-hex" }
nu-protocol = { path = "../nu-protocol", version = "0.106.0", default-features = false }
nu-utils = { path = "../nu-utils", version = "0.106.0", default-features = false }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.106.2" }
nu-engine = { path = "../nu-engine", version = "0.106.2", default-features = false }
nu-json = { version = "0.106.2", path = "../nu-json" }
nu-parser = { path = "../nu-parser", version = "0.106.2" }
nu-pretty-hex = { version = "0.106.2", path = "../nu-pretty-hex" }
nu-protocol = { path = "../nu-protocol", version = "0.106.2", default-features = false }
nu-utils = { path = "../nu-utils", version = "0.106.2", default-features = false }
# Potential dependencies for extras
heck = { workspace = true }
@ -37,6 +37,6 @@ itertools = { workspace = true }
mime = { workspace = true }
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.106.0" }
nu-command = { path = "../nu-command", version = "0.106.0" }
nu-test-support = { path = "../nu-test-support", version = "0.106.0" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.106.2" }
nu-command = { path = "../nu-command", version = "0.106.2" }
nu-test-support = { path = "../nu-test-support", version = "0.106.2" }

View File

@ -3,7 +3,12 @@ use nu_protocol::engine::Command;
#[cfg(test)]
pub fn test_examples(cmd: impl Command + 'static) {
test_examples::test_examples(cmd);
test_examples::test_examples(cmd, &[]);
}
#[cfg(test)]
pub fn test_examples_with_commands(cmd: impl Command + 'static, commands: &[&dyn Command]) {
test_examples::test_examples(cmd, commands);
}
#[cfg(test)]
@ -21,10 +26,10 @@ mod test_examples {
};
use std::collections::HashSet;
pub fn test_examples(cmd: impl Command + 'static) {
pub fn test_examples(cmd: impl Command + 'static, commands: &[&dyn Command]) {
let examples = cmd.examples();
let signature = cmd.signature();
let mut engine_state = make_engine_state(cmd.clone_box());
let mut engine_state = make_engine_state(cmd.clone_box(), commands);
let cwd = std::env::current_dir().expect("Could not get current working directory.");
@ -38,7 +43,7 @@ mod test_examples {
check_example_input_and_output_types_match_command_signature(
&example,
&cwd,
&mut make_engine_state(cmd.clone_box()),
&mut make_engine_state(cmd.clone_box(), commands),
&signature.input_output_types,
signature.operates_on_cell_paths(),
),
@ -57,7 +62,7 @@ mod test_examples {
);
}
fn make_engine_state(cmd: Box<dyn Command>) -> Box<EngineState> {
fn make_engine_state(cmd: Box<dyn Command>, commands: &[&dyn Command]) -> Box<EngineState> {
let mut engine_state = Box::new(EngineState::new());
let delta = {
@ -69,6 +74,10 @@ mod test_examples {
working_set.add_decl(Box::new(nu_cmd_lang::If));
working_set.add_decl(Box::new(nu_command::MathRound));
for command in commands {
working_set.add_decl(command.clone_box());
}
// Adding the command that is being tested to the working set
working_set.add_decl(cmd);
working_set.render()

View File

@ -72,7 +72,7 @@ impl Command for EachWhile {
let metadata = input.metadata();
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(Value::Range { .. }, ..)
| PipelineData::Value(Value::List { .. }, ..)
| PipelineData::ListStream(..) => {
@ -109,7 +109,7 @@ impl Command for EachWhile {
.fuse()
.into_pipeline_data(head, engine_state.signals().clone()))
} else {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
}
// This match allows non-iterables to be accepted,

View File

@ -55,7 +55,7 @@ fn from_url(input: PipelineData, head: Span) -> Result<PipelineData, ShellError>
.map(|(k, v)| (k, Value::string(v, head)))
.collect();
Ok(PipelineData::Value(Value::record(record, head), metadata))
Ok(PipelineData::value(Value::record(record, head), metadata))
}
_ => Err(ShellError::UnsupportedInput {
msg: "String not compatible with URL encoding".to_string(),

View File

@ -109,18 +109,26 @@ impl Command for ToHtml {
"produce a color table of all available themes",
Some('l'),
)
.switch("raw", "do not escape html tags", Some('r'))
.category(Category::Formats)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Outputs an HTML string representing the contents of this table",
description: "Outputs an HTML string representing the contents of this table",
example: "[[foo bar]; [1 2]] | to html",
result: Some(Value::test_string(
r#"<html><style>body { background-color:white;color:black; }</style><body><table><thead><tr><th>foo</th><th>bar</th></tr></thead><tbody><tr><td>1</td><td>2</td></tr></tbody></table></body></html>"#,
)),
},
Example {
description: "Outputs an HTML string using a record of xml data",
example: r#"{tag: a attributes: { style: "color: red" } content: ["hello!"] } | to xml | to html --raw"#,
result: Some(Value::test_string(
r#"<html><style>body { background-color:white;color:black; }</style><body><a style="color: red">hello!</a></body></html>"#,
)),
},
Example {
description: "Optionally, only output the html for the content itself",
example: "[[foo bar]; [1 2]] | to html --partial",
@ -254,6 +262,7 @@ fn to_html(
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 raw = call.has_flag(engine_state, stack, "raw")?;
let theme: Option<Spanned<String>> = call.get_flag(engine_state, stack, "theme")?;
let config = &stack.get_config(engine_state);
@ -319,15 +328,15 @@ fn to_html(
let inner_value = match vec_of_values.len() {
0 => String::default(),
1 => match headers {
Some(headers) => html_table(vec_of_values, headers, config),
Some(headers) => html_table(vec_of_values, headers, raw, config),
None => {
let value = &vec_of_values[0];
html_value(value.clone(), config)
html_value(value.clone(), raw, config)
}
},
_ => match headers {
Some(headers) => html_table(vec_of_values, headers, config),
None => html_list(vec_of_values, config),
Some(headers) => html_table(vec_of_values, headers, raw, config),
None => html_list(vec_of_values, raw, config),
},
};
@ -395,19 +404,19 @@ fn theme_demo(span: Span) -> PipelineData {
})
}
fn html_list(list: Vec<Value>, config: &Config) -> String {
fn html_list(list: Vec<Value>, raw: bool, config: &Config) -> String {
let mut output_string = String::new();
output_string.push_str("<ol>");
for value in list {
output_string.push_str("<li>");
output_string.push_str(&html_value(value, config));
output_string.push_str(&html_value(value, raw, config));
output_string.push_str("</li>");
}
output_string.push_str("</ol>");
output_string
}
fn html_table(table: Vec<Value>, headers: Vec<String>, config: &Config) -> String {
fn html_table(table: Vec<Value>, headers: Vec<String>, raw: bool, config: &Config) -> String {
let mut output_string = String::new();
output_string.push_str("<table>");
@ -430,7 +439,7 @@ fn html_table(table: Vec<Value>, headers: Vec<String>, config: &Config) -> Strin
.cloned()
.unwrap_or_else(|| Value::nothing(span));
output_string.push_str("<td>");
output_string.push_str(&html_value(data, config));
output_string.push_str(&html_value(data, raw, config));
output_string.push_str("</td>");
}
output_string.push_str("</tr>");
@ -441,7 +450,7 @@ fn html_table(table: Vec<Value>, headers: Vec<String>, config: &Config) -> Strin
output_string
}
fn html_value(value: Value, config: &Config) -> String {
fn html_value(value: Value, raw: bool, config: &Config) -> String {
let mut output_string = String::new();
match value {
Value::Binary { val, .. } => {
@ -450,11 +459,22 @@ fn html_value(value: Value, config: &Config) -> String {
output_string.push_str(&output);
output_string.push_str("</pre>");
}
other => output_string.push_str(
&v_htmlescape::escape(&other.to_abbreviated_string(config))
.to_string()
.replace('\n', "<br>"),
),
other => {
if raw {
output_string.push_str(
&other
.to_abbreviated_string(config)
.to_string()
.replace('\n', "<br>"),
)
} else {
output_string.push_str(
&v_htmlescape::escape(&other.to_abbreviated_string(config))
.to_string()
.replace('\n', "<br>"),
)
}
}
}
output_string
}
@ -717,9 +737,10 @@ mod tests {
#[test]
fn test_examples() {
use crate::test_examples;
use crate::test_examples_with_commands;
use nu_command::ToXml;
test_examples(ToHtml {})
test_examples_with_commands(ToHtml {}, &[&ToXml])
}
#[test]

View File

@ -113,7 +113,7 @@ fn format_bits(
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
if let PipelineData::ByteStream(stream, metadata) = input {
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
byte_stream_to_bits(stream, head),
metadata,
))

View File

@ -191,7 +191,7 @@ fn format(
// 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)),
Ok(value) => Ok(PipelineData::value(Value::string(value, head_span), None)),
Err(value) => Err(value),
},

View File

@ -4,4 +4,4 @@ pub mod extra;
pub use extra::*;
#[cfg(test)]
pub use example_test::test_examples;
pub use example_test::{test_examples, test_examples_with_commands};

View File

@ -6,7 +6,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
edition = "2024"
license = "MIT"
name = "nu-cmd-lang"
version = "0.106.0"
version = "0.106.2"
[lib]
bench = false
@ -15,12 +15,12 @@ bench = false
workspace = true
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.106.0", default-features = false }
nu-experimental = { path = "../nu-experimental", version = "0.106.0" }
nu-parser = { path = "../nu-parser", version = "0.106.0" }
nu-protocol = { path = "../nu-protocol", version = "0.106.0", default-features = false }
nu-utils = { path = "../nu-utils", version = "0.106.0", default-features = false }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.106.0" }
nu-engine = { path = "../nu-engine", version = "0.106.2", default-features = false }
nu-experimental = { path = "../nu-experimental", version = "0.106.2" }
nu-parser = { path = "../nu-parser", version = "0.106.2" }
nu-protocol = { path = "../nu-protocol", version = "0.106.2", default-features = false }
nu-utils = { path = "../nu-utils", version = "0.106.2", default-features = false }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.106.2" }
itertools = { workspace = true }
shadow-rs = { version = "1.2", default-features = false }

View File

@ -157,12 +157,12 @@ impl Command for Do {
if !stderr_msg.is_empty() {
child.stderr = Some(ChildPipe::Tee(Box::new(Cursor::new(stderr_msg))));
}
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
ByteStream::child(child, span),
metadata,
))
}
Err(stream) => Ok(PipelineData::ByteStream(stream, metadata)),
Err(stream) => Ok(PipelineData::byte_stream(stream, metadata)),
}
}
Ok(PipelineData::ByteStream(mut stream, metadata))
@ -176,7 +176,7 @@ impl Command for Do {
if let ByteStreamSource::Child(child) = stream.source_mut() {
child.ignore_error(true);
}
Ok(PipelineData::ByteStream(stream, metadata))
Ok(PipelineData::byte_stream(stream, metadata))
}
Ok(PipelineData::Value(Value::Error { .. }, ..)) | Err(_) if ignore_all_errors => {
Ok(PipelineData::empty())
@ -189,7 +189,7 @@ impl Command for Do {
value
}
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
r => r,
}

View File

@ -5,7 +5,7 @@ edition = "2024"
license = "MIT"
name = "nu-cmd-plugin"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
version = "0.106.0"
version = "0.106.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,10 +13,10 @@ version = "0.106.0"
workspace = true
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.106.0" }
nu-path = { path = "../nu-path", version = "0.106.0" }
nu-protocol = { path = "../nu-protocol", version = "0.106.0", features = ["plugin"] }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.106.0" }
nu-engine = { path = "../nu-engine", version = "0.106.2" }
nu-path = { path = "../nu-path", version = "0.106.2" }
nu-protocol = { path = "../nu-protocol", version = "0.106.2", features = ["plugin"] }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.106.2" }
itertools = { workspace = true }

View File

@ -66,7 +66,7 @@ impl Command for PluginStop {
}
if found {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
} else {
Err(ShellError::GenericError {
error: format!("Failed to stop the `{}` plugin", name.item),

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-color-confi
edition = "2024"
license = "MIT"
name = "nu-color-config"
version = "0.106.0"
version = "0.106.2"
[lib]
bench = false
@ -14,12 +14,12 @@ bench = false
workspace = true
[dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.106.0", default-features = false }
nu-engine = { path = "../nu-engine", version = "0.106.0", default-features = false }
nu-json = { path = "../nu-json", version = "0.106.0" }
nu-protocol = { path = "../nu-protocol", version = "0.106.2", default-features = false }
nu-engine = { path = "../nu-engine", version = "0.106.2", default-features = false }
nu-json = { path = "../nu-json", version = "0.106.2" }
nu-ansi-term = { workspace = true }
serde = { workspace = true, features = ["derive"] }
[dev-dependencies]
nu-test-support = { path = "../nu-test-support", version = "0.106.0" }
nu-test-support = { path = "../nu-test-support", version = "0.106.2" }

View File

@ -5,7 +5,7 @@ edition = "2024"
license = "MIT"
name = "nu-command"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
version = "0.106.0"
version = "0.106.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -17,21 +17,21 @@ workspace = true
[dependencies]
nu-ansi-term = { workspace = true }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.106.0" }
nu-color-config = { path = "../nu-color-config", version = "0.106.0" }
nu-engine = { path = "../nu-engine", version = "0.106.0", default-features = false }
nu-experimental = { path = "../nu-experimental", version = "0.106.0" }
nu-glob = { path = "../nu-glob", version = "0.106.0" }
nu-json = { path = "../nu-json", version = "0.106.0" }
nu-parser = { path = "../nu-parser", version = "0.106.0" }
nu-path = { path = "../nu-path", version = "0.106.0" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.106.0" }
nu-protocol = { path = "../nu-protocol", version = "0.106.0", default-features = false }
nu-system = { path = "../nu-system", version = "0.106.0" }
nu-table = { path = "../nu-table", version = "0.106.0" }
nu-term-grid = { path = "../nu-term-grid", version = "0.106.0" }
nu-utils = { path = "../nu-utils", version = "0.106.0", default-features = false }
nuon = { path = "../nuon", version = "0.106.0" }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.106.2" }
nu-color-config = { path = "../nu-color-config", version = "0.106.2" }
nu-engine = { path = "../nu-engine", version = "0.106.2", default-features = false }
nu-experimental = { path = "../nu-experimental", version = "0.106.2" }
nu-glob = { path = "../nu-glob", version = "0.106.2" }
nu-json = { path = "../nu-json", version = "0.106.2" }
nu-parser = { path = "../nu-parser", version = "0.106.2" }
nu-path = { path = "../nu-path", version = "0.106.2" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.106.2" }
nu-protocol = { path = "../nu-protocol", version = "0.106.2", default-features = false }
nu-system = { path = "../nu-system", version = "0.106.2" }
nu-table = { path = "../nu-table", version = "0.106.2" }
nu-term-grid = { path = "../nu-term-grid", version = "0.106.2" }
nu-utils = { path = "../nu-utils", version = "0.106.2", default-features = false }
nuon = { path = "../nuon", version = "0.106.2" }
alphanumeric-sort = { workspace = true }
base64 = { workspace = true }
@ -54,12 +54,14 @@ devicons = { workspace = true }
dialoguer = { workspace = true, default-features = false, features = [
"fuzzy-select",
] }
fuzzy-matcher = { workspace = true }
digest = { workspace = true, default-features = false }
dtparse = { workspace = true }
encoding_rs = { workspace = true }
fancy-regex = { workspace = true }
filesize = { workspace = true }
filetime = { workspace = true }
http = {workspace = true}
human-date-parser = { workspace = true }
indexmap = { workspace = true }
indicatif = { workspace = true }
@ -92,6 +94,7 @@ rusqlite = { workspace = true, features = [
"bundled",
"backup",
"chrono",
"column_decltype",
], optional = true }
rustls = { workspace = true, optional = true, features = ["ring"] }
rustls-native-certs = { workspace = true, optional = true }
@ -191,6 +194,7 @@ os = [
"uu_uname",
"uu_whoami",
"which",
"ureq/platform-verifier"
]
# The dependencies listed below need 'getrandom'.
@ -219,7 +223,7 @@ rustls-tls = [
"dep:rustls-native-certs",
"dep:webpki-roots",
"update-informer/rustls-tls",
"ureq/tls", # ureq 3 will has the feature rustls instead
"ureq/rustls",
]
plugin = ["nu-parser/plugin", "os"]
@ -227,8 +231,8 @@ sqlite = ["rusqlite"]
trash-support = ["trash"]
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.106.0" }
nu-test-support = { path = "../nu-test-support", version = "0.106.0" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.106.2" }
nu-test-support = { path = "../nu-test-support", version = "0.106.2" }
dirs = { workspace = true }
mockito = { workspace = true, default-features = false }

View File

@ -76,7 +76,7 @@ impl Command for BytesAt {
if let PipelineData::ByteStream(stream, metadata) = input {
let stream = stream.slice(call.head, call.arguments_span(), range)?;
Ok(PipelineData::ByteStream(stream, metadata))
Ok(PipelineData::byte_stream(stream, metadata))
} else {
operate(
map_value,

View File

@ -67,7 +67,7 @@ impl Command for BytesCollect {
ByteStreamType::Binary,
);
Ok(PipelineData::ByteStream(output, metadata))
Ok(PipelineData::byte_stream(output, metadata))
}
fn examples(&self) -> Vec<Example> {

View File

@ -129,7 +129,7 @@ fn into_binary(
if let PipelineData::ByteStream(stream, metadata) = input {
// Just set the type - that should be good enough
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
stream.with_type(ByteStreamType::Binary),
metadata,
))

View File

@ -82,9 +82,6 @@ impl Command for IntoDatetime {
(Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Date))),
(Type::table(), Type::table()),
(Type::Nothing, Type::table()),
// FIXME: https://github.com/nushell/nushell/issues/15485
// 'record -> any' was added as a temporary workaround to avoid type inference issues. The Any arm needs to be appear first.
(Type::record(), Type::Any),
(Type::record(), Type::record()),
(Type::record(), Type::Date),
// FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors

View File

@ -53,9 +53,6 @@ impl Command for IntoDuration {
(Type::Float, Type::Duration),
(Type::String, Type::Duration),
(Type::Duration, Type::Duration),
// FIXME: https://github.com/nushell/nushell/issues/15485
// 'record -> any' was added as a temporary workaround to avoid type inference issues. The Any arm needs to be appear first.
(Type::record(), Type::Any),
(Type::record(), Type::record()),
(Type::record(), Type::Duration),
(Type::table(), Type::table()),

View File

@ -170,7 +170,7 @@ fn string_helper(
// within a string stream is actually valid UTF-8. But refuse to do it if it was already set
// to binary
if stream.type_().is_string_coercible() {
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
stream.with_type(ByteStreamType::String),
metadata,
))

View File

@ -1,9 +1,12 @@
use crate::database::values::sqlite::{open_sqlite_db, values_to_sql};
use crate::{
MEMORY_DB,
database::values::sqlite::{open_sqlite_db, values_to_sql},
};
use nu_engine::command_prelude::*;
use itertools::Itertools;
use nu_protocol::Signals;
use std::path::Path;
use std::{borrow::Cow, path::Path};
pub const DEFAULT_TABLE_NAME: &str = "main";
@ -76,6 +79,17 @@ impl Command for IntoSqliteDb {
example: "{ foo: bar, baz: quux } | into sqlite filename.db",
result: None,
},
Example {
description: "Insert data that contains records, lists or tables, that will be stored as JSONB columns
These columns will be automatically turned back into nu objects when read directly via cell-path",
example: "{a_record: {foo: bar, baz: quux}, a_list: [1 2 3], a_table: [[a b]; [0 1] [2 3]]} | into sqlite filename.db -t my_table
(open filename.db).my_table.0.a_list",
result: Some(Value::test_list(vec![
Value::test_int(1),
Value::test_int(2),
Value::test_int(3)
]))
}
]
}
}
@ -89,15 +103,25 @@ impl Table {
pub fn new(
db_path: &Spanned<String>,
table_name: Option<Spanned<String>>,
engine_state: &EngineState,
stack: &Stack,
) -> Result<Self, nu_protocol::ShellError> {
let table_name = if let Some(table_name) = table_name {
table_name.item
} else {
DEFAULT_TABLE_NAME.to_string()
let table_name = table_name
.map(|table_name| table_name.item)
.unwrap_or_else(|| DEFAULT_TABLE_NAME.to_string());
let span = db_path.span;
let db_path: Cow<'_, Path> = match db_path.item.as_str() {
MEMORY_DB => Cow::Borrowed(Path::new(&db_path.item)),
item => engine_state
.cwd(Some(stack))?
.join(item)
.to_std_path_buf()
.into(),
};
// create the sqlite database table
let conn = open_sqlite_db(Path::new(&db_path.item), db_path.span)?;
let conn = open_sqlite_db(&db_path, span)?;
Ok(Self { conn, table_name })
}
@ -182,11 +206,12 @@ fn operate(
let span = call.head;
let file_name: Spanned<String> = call.req(engine_state, stack, 0)?;
let table_name: Option<Spanned<String>> = call.get_flag(engine_state, stack, "table-name")?;
let table = Table::new(&file_name, table_name)?;
Ok(action(input, table, span, engine_state.signals())?.into_pipeline_data())
let table = Table::new(&file_name, table_name, engine_state, stack)?;
Ok(action(engine_state, input, table, span, engine_state.signals())?.into_pipeline_data())
}
fn action(
engine_state: &EngineState,
input: PipelineData,
table: Table,
span: Span,
@ -194,17 +219,17 @@ fn action(
) -> Result<Value, ShellError> {
match input {
PipelineData::ListStream(stream, _) => {
insert_in_transaction(stream.into_iter(), span, table, signals)
insert_in_transaction(engine_state, stream.into_iter(), span, table, signals)
}
PipelineData::Value(value @ Value::List { .. }, _) => {
let span = value.span();
let vals = value
.into_list()
.expect("Value matched as list above, but is not a list");
insert_in_transaction(vals.into_iter(), span, table, signals)
insert_in_transaction(engine_state, vals.into_iter(), span, table, signals)
}
PipelineData::Value(val, _) => {
insert_in_transaction(std::iter::once(val), span, table, signals)
insert_in_transaction(engine_state, std::iter::once(val), span, table, signals)
}
_ => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "list".into(),
@ -216,6 +241,7 @@ fn action(
}
fn insert_in_transaction(
engine_state: &EngineState,
stream: impl Iterator<Item = Value>,
span: Span,
mut table: Table,
@ -272,7 +298,7 @@ fn insert_in_transaction(
inner: Vec::new(),
})?;
let result = insert_value(stream_value, &mut insert_statement);
let result = insert_value(engine_state, stream_value, span, &mut insert_statement);
insert_statement
.finalize()
@ -299,13 +325,15 @@ fn insert_in_transaction(
}
fn insert_value(
engine_state: &EngineState,
stream_value: Value,
call_span: Span,
insert_statement: &mut rusqlite::Statement<'_>,
) -> Result<(), ShellError> {
match stream_value {
// map each column value into its SQL representation
Value::Record { val, .. } => {
let sql_vals = values_to_sql(val.values().cloned())?;
let sql_vals = values_to_sql(engine_state, val.values().cloned(), call_span)?;
insert_statement
.execute(rusqlite::params_from_iter(sql_vals))
@ -345,6 +373,7 @@ fn nu_value_to_sqlite_type(val: &Value) -> Result<&'static str, ShellError> {
Type::Date => Ok("DATETIME"),
Type::Duration => Ok("BIGINT"),
Type::Filesize => Ok("INTEGER"),
Type::List(_) | Type::Record(_) | Type::Table(_) => Ok("JSONB"),
// [NOTE] On null values, we just assume TEXT. This could end up
// creating a table where the column type is wrong in the table schema.
@ -358,11 +387,8 @@ fn nu_value_to_sqlite_type(val: &Value) -> Result<&'static str, ShellError> {
| Type::Closure
| Type::Custom(_)
| Type::Error
| Type::List(_)
| Type::Range
| Type::Record(_)
| Type::Glob
| Type::Table(_) => Err(ShellError::OnlySupportsThisInputType {
| Type::Glob => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "sql".into(),
wrong_type: val.get_type().to_string(),
dst_span: Span::unknown(),
@ -388,17 +414,3 @@ fn get_columns_with_sqlite_types(
Ok(columns)
}
#[cfg(test)]
mod tests {
use super::*;
// use super::{action, IntoSqliteDb};
// use nu_protocol::Type::Error;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(IntoSqliteDb {})
}
}

View File

@ -41,7 +41,7 @@ impl Command for QueryDb {
Example {
description: "Execute a SQL statement with parameters",
example: r#"stor create -t my_table -c { first: str, second: int }
stor open | query db "INSERT INTO my_table VALUES (?, ?)" -p [hello 123]"#,
stor open | query db "INSERT INTO my_table VALUES (?, ?)" -p [hello 123]"#,
result: None,
},
Example {
@ -54,6 +54,36 @@ stor open | query db "SELECT * FROM my_table WHERE second = :search_second" -p {
"second" => Value::test_int(123)
})])),
},
Example {
description: "Execute a SQL query, selecting a declared JSON(B) column that will automatically be parsed",
example: r#"stor create -t my_table -c {data: jsonb}
[{data: {name: Albert, age: 40}} {data: {name: Barnaby, age: 54}}] | stor insert -t my_table
stor open | query db "SELECT data FROM my_table WHERE data->>'age' < 45""#,
result: Some(Value::test_list(vec![Value::test_record(record! {
"data" => Value::test_record(
record! {
"name" => Value::test_string("Albert"),
"age" => Value::test_int(40),
}
)})])),
},
Example {
description: "Execute a SQL query selecting a sub-field of a JSON(B) column.
In this case, results must be parsed afterwards because SQLite does not
return declaration types when a JSON(B) column is not directly selected",
example: r#"stor create -t my_table -c {data: jsonb}
stor insert -t my_table -d {data: {foo: foo, bar: 12, baz: [0 1 2]}}
stor open | query db "SELECT data->'baz' AS baz FROM my_table" | update baz {from json}"#,
result: Some(Value::test_list(vec![Value::test_record(
record! { "baz" =>
Value::test_list(vec![
Value::test_int(0),
Value::test_int(1),
Value::test_int(2),
])
},
)])),
},
]
}
@ -73,7 +103,7 @@ stor open | query db "SELECT * FROM my_table WHERE second = :search_second" -p {
.get_flag(engine_state, stack, "params")?
.unwrap_or_else(|| Value::nothing(Span::unknown()));
let params = nu_value_to_params(params_value)?;
let params = nu_value_to_params(engine_state, params_value, call.head)?;
let db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
db.query(&sql, params, call.head)

View File

@ -79,7 +79,7 @@ impl Command for SchemaDb {
// TODO: add views and triggers
Ok(PipelineData::Value(Value::record(record, span), None))
Ok(PipelineData::value(Value::record(record, span), None))
}
}

View File

@ -4,7 +4,7 @@ use super::definitions::{
};
use nu_protocol::{
CustomValue, PipelineData, Record, ShellError, Signals, Span, Spanned, Value,
shell_error::io::IoError,
engine::EngineState, shell_error::io::IoError,
};
use rusqlite::{
Connection, DatabaseName, Error as SqliteError, OpenFlags, Row, Statement, ToSql,
@ -431,35 +431,44 @@ fn run_sql_query(
}
// This is taken from to text local_into_string but tweaks it a bit so that certain formatting does not happen
pub fn value_to_sql(value: Value) -> Result<Box<dyn rusqlite::ToSql>, ShellError> {
Ok(match value {
Value::Bool { val, .. } => Box::new(val),
Value::Int { val, .. } => Box::new(val),
Value::Float { val, .. } => Box::new(val),
Value::Filesize { val, .. } => Box::new(val.get()),
Value::Duration { val, .. } => Box::new(val),
Value::Date { val, .. } => Box::new(val),
Value::String { val, .. } => Box::new(val),
Value::Binary { val, .. } => Box::new(val),
Value::Nothing { .. } => Box::new(rusqlite::types::Null),
pub fn value_to_sql(
engine_state: &EngineState,
value: Value,
call_span: Span,
) -> Result<Box<dyn rusqlite::ToSql>, ShellError> {
match value {
Value::Bool { val, .. } => Ok(Box::new(val)),
Value::Int { val, .. } => Ok(Box::new(val)),
Value::Float { val, .. } => Ok(Box::new(val)),
Value::Filesize { val, .. } => Ok(Box::new(val.get())),
Value::Duration { val, .. } => Ok(Box::new(val)),
Value::Date { val, .. } => Ok(Box::new(val)),
Value::String { val, .. } => Ok(Box::new(val)),
Value::Binary { val, .. } => Ok(Box::new(val)),
Value::Nothing { .. } => Ok(Box::new(rusqlite::types::Null)),
val => {
return Err(ShellError::OnlySupportsThisInputType {
exp_input_type:
"bool, int, float, filesize, duration, date, string, nothing, binary".into(),
wrong_type: val.get_type().to_string(),
dst_span: Span::unknown(),
src_span: val.span(),
});
let json_value = crate::value_to_json_value(engine_state, &val, call_span, false)?;
match nu_json::to_string_raw(&json_value) {
Ok(s) => Ok(Box::new(s)),
Err(err) => Err(ShellError::CantConvert {
to_type: "JSON".into(),
from_type: val.get_type().to_string(),
span: val.span(),
help: Some(err.to_string()),
}),
}
}
})
}
}
pub fn values_to_sql(
engine_state: &EngineState,
values: impl IntoIterator<Item = Value>,
call_span: Span,
) -> Result<Vec<Box<dyn rusqlite::ToSql>>, ShellError> {
values
.into_iter()
.map(value_to_sql)
.map(|v| value_to_sql(engine_state, v, call_span))
.collect::<Result<Vec<_>, _>>()
}
@ -474,13 +483,17 @@ impl Default for NuSqlParams {
}
}
pub fn nu_value_to_params(value: Value) -> Result<NuSqlParams, ShellError> {
pub fn nu_value_to_params(
engine_state: &EngineState,
value: Value,
call_span: Span,
) -> Result<NuSqlParams, ShellError> {
match value {
Value::Record { val, .. } => {
let mut params = Vec::with_capacity(val.len());
for (mut column, value) in val.into_owned().into_iter() {
let sql_type_erased = value_to_sql(value)?;
let sql_type_erased = value_to_sql(engine_state, value, call_span)?;
if !column.starts_with([':', '@', '$']) {
column.insert(0, ':');
@ -495,7 +508,7 @@ pub fn nu_value_to_params(value: Value) -> Result<NuSqlParams, ShellError> {
let mut params = Vec::with_capacity(vals.len());
for value in vals.into_iter() {
let sql_type_erased = value_to_sql(value)?;
let sql_type_erased = value_to_sql(engine_state, value, call_span)?;
params.push(sql_type_erased);
}
@ -557,17 +570,49 @@ fn read_single_table(
prepared_statement_to_nu_list(stmt, NuSqlParams::default(), call_span, signals)
}
/// The SQLite type behind a query column returned as some raw type (e.g. 'text')
#[derive(Clone, Copy)]
pub enum DeclType {
Json,
Jsonb,
}
impl DeclType {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"JSON" => Some(DeclType::Json),
"JSONB" => Some(DeclType::Jsonb),
_ => None, // We are only special-casing JSON(B) columns for now
}
}
}
/// A column out of an SQLite query, together with its type
pub struct TypedColumn {
pub name: String,
pub decl_type: Option<DeclType>,
}
impl TypedColumn {
pub fn from_rusqlite_column(c: &rusqlite::Column) -> Self {
Self {
name: c.name().to_owned(),
decl_type: c.decl_type().and_then(DeclType::from_str),
}
}
}
fn prepared_statement_to_nu_list(
mut stmt: Statement,
params: NuSqlParams,
call_span: Span,
signals: &Signals,
) -> Result<Value, SqliteOrShellError> {
let column_names = stmt
.column_names()
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
let columns: Vec<TypedColumn> = stmt
.columns()
.iter()
.map(TypedColumn::from_rusqlite_column)
.collect();
// I'm very sorry for this repetition
// I tried scoping the match arms to the query_map alone, but lifetime and closure reference escapes
@ -577,11 +622,7 @@ fn prepared_statement_to_nu_list(
let refs: Vec<&dyn ToSql> = params.iter().map(|value| (&**value)).collect();
let row_results = stmt.query_map(refs.as_slice(), |row| {
Ok(convert_sqlite_row_to_nu_value(
row,
call_span,
&column_names,
))
Ok(convert_sqlite_row_to_nu_value(row, call_span, &columns))
})?;
// we collect all rows before returning them. Not ideal but it's hard/impossible to return a stream from a CustomValue
@ -603,11 +644,7 @@ fn prepared_statement_to_nu_list(
.collect();
let row_results = stmt.query_map(refs.as_slice(), |row| {
Ok(convert_sqlite_row_to_nu_value(
row,
call_span,
&column_names,
))
Ok(convert_sqlite_row_to_nu_value(row, call_span, &columns))
})?;
// we collect all rows before returning them. Not ideal but it's hard/impossible to return a stream from a CustomValue
@ -650,14 +687,14 @@ fn read_entire_sqlite_db(
Ok(Value::record(tables, call_span))
}
pub fn convert_sqlite_row_to_nu_value(row: &Row, span: Span, column_names: &[String]) -> Value {
let record = column_names
pub fn convert_sqlite_row_to_nu_value(row: &Row, span: Span, columns: &[TypedColumn]) -> Value {
let record = columns
.iter()
.enumerate()
.map(|(i, col)| {
(
col.clone(),
convert_sqlite_value_to_nu_value(row.get_ref_unwrap(i), span),
col.name.clone(),
convert_sqlite_value_to_nu_value(row.get_ref_unwrap(i), col.decl_type, span),
)
})
.collect();
@ -665,18 +702,25 @@ pub fn convert_sqlite_row_to_nu_value(row: &Row, span: Span, column_names: &[Str
Value::record(record, span)
}
pub fn convert_sqlite_value_to_nu_value(value: ValueRef, span: Span) -> Value {
pub fn convert_sqlite_value_to_nu_value(
value: ValueRef,
decl_type: Option<DeclType>,
span: Span,
) -> Value {
match value {
ValueRef::Null => Value::nothing(span),
ValueRef::Integer(i) => Value::int(i, span),
ValueRef::Real(f) => Value::float(f, span),
ValueRef::Text(buf) => {
let s = match std::str::from_utf8(buf) {
Ok(v) => v,
Err(_) => return Value::error(ShellError::NonUtf8 { span }, span),
};
Value::string(s.to_string(), span)
}
ValueRef::Text(buf) => match (std::str::from_utf8(buf), decl_type) {
(Ok(txt), Some(DeclType::Json | DeclType::Jsonb)) => {
match crate::convert_json_string_to_value(txt, span) {
Ok(val) => val,
Err(err) => Value::error(err, span),
}
}
(Ok(txt), _) => Value::string(txt.to_string(), span),
(Err(_), _) => Value::error(ShellError::NonUtf8 { span }, span),
},
ValueRef::Blob(u) => Value::binary(u.to_vec(), span),
}
}

View File

@ -42,7 +42,7 @@ impl Command for Debug {
let raw = call.has_flag(engine_state, stack, "raw")?;
let raw_value = call.has_flag(engine_state, stack, "raw-value")?;
// Should PipelineData::Empty result in an error here?
// Should PipelineData::empty() result in an error here?
input.map(
move |x| {

View File

@ -25,7 +25,7 @@ impl Command for DebugEnv {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(PipelineData::Value(
Ok(PipelineData::value(
env_to_strings(engine_state, stack)?.into_value(call.head),
None,
))

View File

@ -35,7 +35,7 @@ impl Command for DebugExperimentalOptions {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(PipelineData::Value(
Ok(PipelineData::value(
Value::list(
nu_experimental::ALL
.iter()

View File

@ -124,7 +124,7 @@ pub(super) fn start_editor(
let post_wait_callback = PostWaitCallback::for_job_control(engine_state, None, None);
// Wrap the output into a `PipelineData::ByteStream`.
// Wrap the output into a `PipelineData::byte_stream`.
let child = nu_protocol::process::ChildProcess::new(
child,
None,
@ -133,7 +133,7 @@ pub(super) fn start_editor(
Some(post_wait_callback),
)?;
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
ByteStream::child(child, call.head),
None,
))

View File

@ -33,7 +33,7 @@ impl Command for ConfigUseColors {
.get_config()
.use_ansi_coloring
.get(engine_state);
Ok(PipelineData::Value(
Ok(PipelineData::value(
Value::bool(use_ansi_coloring, call.head),
None,
))

View File

@ -32,7 +32,7 @@ Messages may have numeric flags attached to them. This commands supports filteri
If no tag is specified, this command will accept any message.
If no message with the specified tag (if any) is available in the mailbox, this command will block the current thread until one arrives.
By default this command block indefinitely until a matching message arrives, but a timeout duration can be specified.
By default this command block indefinitely until a matching message arrives, but a timeout duration can be specified.
If a timeout duration of zero is specified, it will succeed only if there already is a message in the mailbox.
Note: When using par-each, only one thread at a time can utilize this command.
@ -78,9 +78,7 @@ in no particular order, regardless of the specified timeout parameter.
let tag = tag_arg.map(|it| it.item as FilterTag);
let duration: Option<i64> = call.get_flag(engine_state, stack, "timeout")?;
let timeout = duration.map(|it| Duration::from_nanos(it as u64));
let timeout: Option<Duration> = call.get_flag(engine_state, stack, "timeout")?;
let mut mailbox = engine_state
.current_job
@ -116,6 +114,11 @@ in no particular order, regardless of the specified timeout parameter.
description: "Get a message or fail if no message is available immediately",
result: None,
},
Example {
example: "job spawn { sleep 1sec; 'hi' | job send 0 }; job recv",
description: "Receive a message from a newly-spawned job",
result: None,
},
]
}
}

View File

@ -17,7 +17,7 @@ impl Command for JobSend {
r#"
This command sends a message to a background job, which can then read sent messages
in a first-in-first-out fashion with `job recv`. When it does so, it may additionally specify a numeric filter tag,
in which case it will only read messages sent with the exact same filter tag.
in which case it will only read messages sent with the exact same filter tag.
In particular, the id 0 refers to the main/initial nushell thread.
A message can be any nushell value, and streams are always collected before being sent.
@ -101,10 +101,17 @@ This command never blocks.
}
fn examples(&self) -> Vec<Example> {
vec![Example {
example: "let id = job spawn { job recv | save sent.txt }; 'hi' | job send $id",
description: "Send a message to a newly spawned job",
result: None,
}]
vec![
Example {
example: "let id = job spawn { job recv | save sent.txt }; 'hi' | job send $id",
description: "Send a message from the main thread to a newly-spawned job",
result: None,
},
Example {
example: "job spawn { sleep 1sec; 'hi' | job send 0 }; job recv",
description: "Send a message from a newly-spawned job to the main thread (which always has an ID of 0)",
result: None,
},
]
}
}

View File

@ -120,6 +120,6 @@ impl Command for Mktemp {
});
}
};
Ok(PipelineData::Value(Value::string(res, span), None))
Ok(PipelineData::value(Value::string(res, span), None))
}
}

View File

@ -176,7 +176,7 @@ impl Command for Open {
.map_err(|err| IoError::new(err, arg_span, PathBuf::from(path)))?;
// No content_type by default - Is added later if no converter is found
let stream = PipelineData::ByteStream(
let stream = PipelineData::byte_stream(
ByteStream::file(file, call_span, engine_state.signals().clone()),
Some(PipelineMetadata {
data_source: DataSource::FilePath(path.to_path_buf()),
@ -246,7 +246,7 @@ impl Command for Open {
}
if output.is_empty() {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
} else if output.len() == 1 {
Ok(output.remove(0))
} else {

View File

@ -339,7 +339,7 @@ fn rm(
inner: vec![],
});
} else if !confirmed {
return Ok(PipelineData::Empty);
return Ok(PipelineData::empty());
}
}

View File

@ -191,7 +191,7 @@ impl Command for Save {
}
}
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
PipelineData::ListStream(ls, pipeline_metadata)
if raw || prepare_path(&path, append, force)?.0.extension().is_none() =>

View File

@ -45,7 +45,7 @@ impl Command for Start {
// Attempt to parse the input as a URL
if let Ok(url) = url::Url::parse(path_no_whitespace) {
open_path(url.as_str(), engine_state, stack, path.span)?;
return Ok(PipelineData::Empty);
return Ok(PipelineData::empty());
}
// If it's not a URL, treat it as a file path
let cwd = engine_state.cwd(Some(stack))?;
@ -54,7 +54,7 @@ impl Command for Start {
// Check if the path exists or if it's a valid file/directory
if full_path.exists() {
open_path(full_path, engine_state, stack, path.span)?;
return Ok(PipelineData::Empty);
return Ok(PipelineData::empty());
}
// If neither file nor URL, return an error
Err(ShellError::GenericError {

View File

@ -6,7 +6,11 @@ use notify_debouncer_full::{
},
};
use nu_engine::{ClosureEval, command_prelude::*};
use nu_protocol::{engine::Closure, report_shell_error, shell_error::io::IoError};
use nu_protocol::{
DeprecationEntry, DeprecationType, ReportMode, engine::Closure, report_shell_error,
shell_error::io::IoError,
};
use std::{
path::PathBuf,
sync::mpsc::{RecvTimeoutError, channel},
@ -33,9 +37,20 @@ impl Command for Watch {
vec!["watcher", "reload", "filesystem"]
}
fn deprecation_info(&self) -> Vec<DeprecationEntry> {
vec![DeprecationEntry {
ty: DeprecationType::Flag("--debounce-ms".into()),
report_mode: ReportMode::FirstUse,
since: Some("0.107.0".into()),
expected_removal: Some("0.109.0".into()),
help: Some("`--debounce-ms` will be removed in favour of `--debounce`".into()),
}]
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("watch")
.input_output_types(vec![(Type::Nothing, Type::table())])
// actually `watch` never returns normally, but we don't have `noreturn` / `never` type yet
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.required("path", SyntaxShape::Filepath, "The path to watch. Can be a file or directory.")
.required("closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::String, SyntaxShape::String, SyntaxShape::String])),
@ -43,7 +58,13 @@ impl Command for Watch {
.named(
"debounce-ms",
SyntaxShape::Int,
"Debounce changes for this many milliseconds (default: 100). Adjust if you find that single writes are reported as multiple events",
"Debounce changes for this many milliseconds (default: 100). Adjust if you find that single writes are reported as multiple events (deprecated)",
None,
)
.named(
"debounce",
SyntaxShape::Duration,
"Debounce changes for this duration (default: 100ms). Adjust if you find that single writes are reported as multiple events",
Some('d'),
)
.named(
@ -95,20 +116,14 @@ impl Command for Watch {
let quiet = call.has_flag(engine_state, stack, "quiet")?;
let debounce_duration_flag: Option<Spanned<i64>> =
let debounce_duration_flag_ms: Option<Spanned<i64>> =
call.get_flag(engine_state, stack, "debounce-ms")?;
let debounce_duration = match debounce_duration_flag {
Some(val) => match u64::try_from(val.item) {
Ok(val) => Duration::from_millis(val),
Err(_) => {
return Err(ShellError::TypeMismatch {
err_message: "Debounce duration is invalid".to_string(),
span: val.span,
});
}
},
None => DEFAULT_WATCH_DEBOUNCE_DURATION,
};
let debounce_duration_flag: Option<Spanned<Duration>> =
call.get_flag(engine_state, stack, "debounce")?;
let debounce_duration: Duration =
resolve_duration_arguments(debounce_duration_flag_ms, debounce_duration_flag)?;
let glob_flag: Option<Spanned<String>> = call.get_flag(engine_state, stack, "glob")?;
let glob_pattern = match glob_flag {
@ -196,7 +211,7 @@ impl Command for Watch {
new_path.unwrap_or_else(|| "".into()).to_string_lossy(),
head,
))
.run_with_input(PipelineData::Empty);
.run_with_input(PipelineData::empty());
match result {
Ok(val) => val.print_table(engine_state, stack, false, false)?,
@ -294,6 +309,11 @@ impl Command for Watch {
example: r#"watch /foo/bar { |op, path| $"($op) - ($path)(char nl)" | save --append changes_in_bar.log }"#,
result: None,
},
Example {
description: "Print file changes with a debounce time of 5 minutes",
example: r#"watch /foo/bar --debounce 5min { |op, path| $"Registered ($op) on ($path)" | print }"#,
result: None,
},
Example {
description: "Note: if you are looking to run a command every N units of time, this can be accomplished with a loop and sleep",
example: r#"loop { command; sleep duration }"#,
@ -302,3 +322,26 @@ impl Command for Watch {
]
}
}
fn resolve_duration_arguments(
debounce_duration_flag_ms: Option<Spanned<i64>>,
debounce_duration_flag: Option<Spanned<Duration>>,
) -> Result<Duration, ShellError> {
match (debounce_duration_flag, debounce_duration_flag_ms) {
(None, None) => Ok(DEFAULT_WATCH_DEBOUNCE_DURATION),
(Some(l), Some(r)) => Err(ShellError::IncompatibleParameters {
left_message: "Here".to_string(),
left_span: l.span,
right_message: "and here".to_string(),
right_span: r.span,
}),
(None, Some(val)) => match u64::try_from(val.item) {
Ok(v) => Ok(Duration::from_millis(v)),
Err(_) => Err(ShellError::TypeMismatch {
err_message: "Debounce duration is invalid".to_string(),
span: val.span,
}),
},
(Some(v), None) => Ok(v.item),
}
}

View File

@ -199,7 +199,7 @@ pub fn chunk_by(
let metadata = input.metadata();
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(Value::Range { .. }, ..)
| PipelineData::Value(Value::List { .. }, ..)
| PipelineData::ListStream(..) => {

View File

@ -124,11 +124,11 @@ pub fn chunks(
PipelineData::Value(Value::List { vals, .. }, metadata) => {
let chunks = ChunksIter::new(vals, chunk_size, span);
let stream = ListStream::new(chunks, span, engine_state.signals().clone());
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
PipelineData::ListStream(stream, metadata) => {
let stream = stream.modify(|iter| ChunksIter::new(iter, chunk_size, span));
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
PipelineData::Value(Value::Binary { val, .. }, metadata) => {
let chunk_read = ChunkRead {
@ -148,7 +148,7 @@ pub fn chunks(
}
PipelineData::ByteStream(stream, metadata) => {
let pipeline_data = match stream.reader() {
None => PipelineData::Empty,
None => PipelineData::empty(),
Some(reader) => {
let chunk_read = ChunkRead {
reader,

View File

@ -74,7 +74,7 @@ impl Command for Columns {
fn getcol(head: Span, input: PipelineData) -> Result<PipelineData, ShellError> {
let metadata = input.metadata();
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(v, ..) => {
let span = v.span();
let cols = match v {

View File

@ -227,7 +227,7 @@ fn default(
// stream's internal state already preserves the original signals config, so if this
// Signals::empty list stream gets interrupted it will be caught by the underlying iterator
let ls = ListStream::new(stream, span, Signals::empty());
Ok(PipelineData::ListStream(ls, metadata))
Ok(PipelineData::list_stream(ls, metadata))
// Otherwise, return the input as is
} else {
Ok(input)
@ -236,7 +236,7 @@ fn default(
/// A wrapper around the default value to handle closures and caching values
enum DefaultValue {
Uncalculated(Spanned<ClosureEval>),
Uncalculated(Box<Spanned<ClosureEval>>),
Calculated(Value),
}
@ -258,7 +258,7 @@ impl DefaultValue {
match value {
Value::Closure { val, .. } => {
let closure_eval = ClosureEval::new(engine_state, stack, *val);
DefaultValue::Uncalculated(closure_eval.into_spanned(span))
DefaultValue::Uncalculated(Box::new(closure_eval.into_spanned(span)))
}
_ => DefaultValue::Calculated(value),
}
@ -269,7 +269,7 @@ impl DefaultValue {
DefaultValue::Uncalculated(closure) => {
let value = closure
.item
.run_with_input(PipelineData::Empty)?
.run_with_input(PipelineData::empty())?
.into_value(closure.span)?;
*self = DefaultValue::Calculated(value.clone());
Ok(value)
@ -282,7 +282,7 @@ impl DefaultValue {
fn single_run_pipeline_data(self) -> Result<PipelineData, ShellError> {
match self {
DefaultValue::Uncalculated(mut closure) => {
closure.item.run_with_input(PipelineData::Empty)
closure.item.run_with_input(PipelineData::empty())
}
DefaultValue::Calculated(val) => Ok(val.into_pipeline_data()),
}

View File

@ -108,7 +108,7 @@ fn drop_cols(
metadata,
))
} else {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
}
PipelineData::Value(mut v, ..) => {
@ -136,7 +136,7 @@ fn drop_cols(
val => Err(unsupported_value_error(&val, head)),
}
}
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
exp_input_type: "table or record".into(),
wrong_type: stream.type_().describe().into(),

View File

@ -90,6 +90,11 @@ with 'transpose' first."#
Value::nothing(Span::test_data()),
])),
},
Example {
example: r#"$env.name? | each { $"hello ($in)" } | default "bye""#,
description: "Update value if not null, otherwise do nothing",
result: None,
},
]
}
@ -106,7 +111,8 @@ with 'transpose' first."#
let metadata = input.metadata();
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(Value::Nothing { .. }, ..) => Ok(input),
PipelineData::Value(Value::Range { .. }, ..)
| PipelineData::Value(Value::List { .. }, ..)
| PipelineData::ListStream(..) => {
@ -164,7 +170,7 @@ with 'transpose' first."#
})
.into_pipeline_data(head, engine_state.signals().clone()))
} else {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
}
// This match allows non-iterables to be accepted,

View File

@ -28,7 +28,7 @@ pub fn empty(
}
} else {
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::ByteStream(stream, ..) => {
let span = stream.span();
match stream.reader() {

View File

@ -31,12 +31,12 @@ impl Command for Find {
)
.switch(
"ignore-case",
"case-insensitive regex mode; equivalent to (?i)",
"case-insensitive; when in regex mode, this is equivalent to (?i)",
Some('i'),
)
.switch(
"multiline",
"multi-line regex mode: ^ and $ match begin/end of line; equivalent to (?m)",
"don't split multi-line strings into lists of lines. you should use this option when using the (?m) or (?s) flags in regex mode",
Some('m'),
)
.switch(
@ -72,8 +72,8 @@ impl Command for Find {
result: None,
},
Example {
description: "Search and highlight text for a term in a string. Note that regular search is case insensitive",
example: r#"'Cargo.toml' | find cargo"#,
description: "Search and highlight text for a term in a string.",
example: r#"'Cargo.toml' | find Cargo"#,
result: Some(Value::test_string(
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mCargo\u{1b}[0m\u{1b}[37m.toml\u{1b}[0m"
.to_owned(),
@ -81,7 +81,7 @@ impl Command for Find {
},
Example {
description: "Search a number or a file size in a list of numbers",
example: r#"[1 5 3kb 4 3Mb] | find 5 3kb"#,
example: r#"[1 5 3kb 4 35 3Mb] | find 5 3kb"#,
result: Some(Value::list(
vec![Value::test_int(5), Value::test_filesize(3000)],
Span::test_data(),
@ -103,16 +103,16 @@ impl Command for Find {
)),
},
Example {
description: "Find using regex",
example: r#"[abc bde arc abf] | find --regex "ab""#,
description: "Search using regex",
example: r#"[abc odb arc abf] | find --regex "b.""#,
result: Some(Value::list(
vec![
Value::test_string(
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mab\u{1b}[0m\u{1b}[37mc\u{1b}[0m"
"\u{1b}[37ma\u{1b}[0m\u{1b}[41;37mbc\u{1b}[0m\u{1b}[37m\u{1b}[0m"
.to_string(),
),
Value::test_string(
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mab\u{1b}[0m\u{1b}[37mf\u{1b}[0m"
"\u{1b}[37ma\u{1b}[0m\u{1b}[41;37mbf\u{1b}[0m\u{1b}[37m\u{1b}[0m"
.to_string(),
),
],
@ -120,8 +120,8 @@ impl Command for Find {
)),
},
Example {
description: "Find using regex case insensitive",
example: r#"[aBc bde Arc abf] | find --regex "ab" -i"#,
description: "Case insensitive search",
example: r#"[aBc bde Arc abf] | find "ab" -i"#,
result: Some(Value::list(
vec![
Value::test_string(
@ -211,11 +211,33 @@ impl Command for Find {
Span::test_data(),
)),
},
Example {
description: "Find in a multi-line string",
example: r#""Violets are red\nAnd roses are blue\nWhen metamaterials\nAlter their hue" | find "ue""#,
result: Some(Value::list(
vec![
Value::test_string(
"\u{1b}[37mAnd roses are bl\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\u{1b}[0m",
),
Value::test_string(
"\u{1b}[37mAlter their h\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\u{1b}[0m",
),
],
Span::test_data(),
)),
},
Example {
description: "Find in a multi-line string without splitting the input into a list of lines",
example: r#""Violets are red\nAnd roses are blue\nWhen metamaterials\nAlter their hue" | find --multiline "ue""#,
result: Some(Value::test_string(
"\u{1b}[37mViolets are red\nAnd roses are bl\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\nWhen metamaterials\nAlter their h\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\u{1b}[0m",
)),
},
]
}
fn search_terms(&self) -> Vec<&str> {
vec!["filter", "regex", "search", "condition"]
vec!["filter", "regex", "search", "condition", "grep"]
}
fn run(
@ -227,11 +249,25 @@ impl Command for Find {
) -> Result<PipelineData, ShellError> {
let pattern = get_match_pattern_from_arguments(engine_state, stack, call)?;
let multiline = call.has_flag(engine_state, stack, "multiline")?;
let columns_to_search: Vec<_> = call
.get_flag(engine_state, stack, "columns")?
.unwrap_or_default();
let input = split_string_if_multiline(input, call.head);
let input = if multiline {
if let PipelineData::ByteStream(..) = input {
// ByteStream inputs are processed by iterating over the lines, which necessarily
// breaks the multi-line text being streamed into a list of lines.
return Err(ShellError::IncompatibleParametersSingle {
msg: "Flag `--multiline` currently doesn't work for byte stream inputs. Consider using `collect`".into(),
span: call.get_flag_span(stack, "multiline").expect("has flag"),
});
};
input
} else {
split_string_if_multiline(input, call.head)
};
find_in_pipelinedata(pattern, columns_to_search, engine_state, stack, input)
}
@ -242,8 +278,11 @@ struct MatchPattern {
/// the regex to be used for matching in text
regex: Regex,
/// the list of match terms converted to lowercase strings, or empty if a regex was provided
lower_terms: Vec<String>,
/// the list of match terms (converted to lowercase if needed), or empty if a regex was provided
search_terms: Vec<String>,
/// case-insensitive match
ignore_case: bool,
/// return a modified version of the value where matching parts are highlighted
highlight: bool,
@ -272,6 +311,10 @@ fn get_match_pattern_from_arguments(
let invert = call.has_flag(engine_state, stack, "invert")?;
let highlight = !call.has_flag(engine_state, stack, "no-highlight")?;
let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
let dotall = call.has_flag(engine_state, stack, "dotall")?;
let style_computer = StyleComputer::from_config(engine_state, stack);
// Currently, search results all use the same style.
// Also note that this sample string is passed into user-written code (the closure that may or may not be
@ -280,7 +323,7 @@ fn get_match_pattern_from_arguments(
let highlight_style =
style_computer.compute("search_result", &Value::string("search result", span));
let (regex_str, lower_terms) = if let Some(regex) = regex {
let (regex_str, search_terms) = if let Some(regex) = regex {
if !terms.is_empty() {
return Err(ShellError::IncompatibleParametersSingle {
msg: "Cannot use a `--regex` parameter with additional search terms".into(),
@ -288,47 +331,54 @@ fn get_match_pattern_from_arguments(
});
}
let insensitive = call.has_flag(engine_state, stack, "ignore-case")?;
let multiline = call.has_flag(engine_state, stack, "multiline")?;
let dotall = call.has_flag(engine_state, stack, "dotall")?;
let flags = match (insensitive, multiline, dotall) {
(false, false, false) => "",
(true, false, false) => "(?i)", // case insensitive
(false, true, false) => "(?m)", // multi-line mode
(false, false, true) => "(?s)", // allow . to match \n
(true, true, false) => "(?im)", // case insensitive and multi-line mode
(true, false, true) => "(?is)", // case insensitive and allow . to match \n
(false, true, true) => "(?ms)", // multi-line mode and allow . to match \n
(true, true, true) => "(?ims)", // case insensitive, multi-line mode and allow . to match \n
let flags = match (ignore_case, dotall) {
(false, false) => "",
(true, false) => "(?i)", // case insensitive
(false, true) => "(?s)", // allow . to match \n
(true, true) => "(?is)", // case insensitive and allow . to match \n
};
(flags.to_string() + regex.as_str(), Vec::new())
} else {
if dotall {
return Err(ShellError::IncompatibleParametersSingle {
msg: "Flag --dotall only works for regex search".into(),
span: call.get_flag_span(stack, "dotall").expect("has flag"),
});
}
let mut regex = String::new();
regex += "(?i)";
if ignore_case {
regex += "(?i)";
}
let lower_terms = terms
let search_terms = terms
.iter()
.map(|v| escape(&v.to_expanded_string("", &config).to_lowercase()).into())
.map(|v| {
if ignore_case {
v.to_expanded_string("", &config).to_lowercase()
} else {
v.to_expanded_string("", &config)
}
})
.collect::<Vec<String>>();
if let Some(term) = lower_terms.first() {
let escaped_terms = search_terms
.iter()
.map(|v| escape(v).into())
.collect::<Vec<String>>();
if let Some(term) = escaped_terms.first() {
regex += term;
}
for term in lower_terms.iter().skip(1) {
for term in escaped_terms.iter().skip(1) {
regex += "|";
regex += term;
}
let lower_terms = terms
.iter()
.map(|v| v.to_expanded_string("", &config).to_lowercase())
.collect::<Vec<String>>();
(regex, lower_terms)
(regex, search_terms)
};
let regex = Regex::new(regex_str.as_str()).map_err(|e| ShellError::TypeMismatch {
@ -338,7 +388,8 @@ fn get_match_pattern_from_arguments(
Ok(MatchPattern {
regex,
lower_terms,
search_terms,
ignore_case,
invert,
highlight,
string_style,
@ -448,7 +499,7 @@ fn find_in_pipelinedata(
let map_columns_to_search = columns_to_search.clone();
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(_, _) => input
.filter(
move |value| {
@ -470,7 +521,7 @@ fn find_in_pipelinedata(
.map(move |x| highlight_matches_in_value(&map_pattern, x, &map_columns_to_search))
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
PipelineData::ByteStream(stream, ..) => {
let span = stream.span();
@ -489,7 +540,7 @@ fn find_in_pipelinedata(
}
Ok(Value::list(output, span).into_pipeline_data())
} else {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
}
}
@ -507,7 +558,11 @@ fn value_should_be_printed(
columns_to_search: &[String],
config: &Config,
) -> bool {
let lower_value = value.to_expanded_string("", config).to_lowercase();
let value_as_string = if pattern.ignore_case {
value.to_expanded_string("", config).to_lowercase()
} else {
value.to_expanded_string("", config)
};
match value {
Value::Bool { .. }
@ -519,18 +574,18 @@ fn value_should_be_printed(
| Value::Float { .. }
| Value::Closure { .. }
| Value::Nothing { .. } => {
if !pattern.lower_terms.is_empty() {
if !pattern.search_terms.is_empty() {
// look for exact match when searching with terms
pattern
.lower_terms
.search_terms
.iter()
.any(|term: &String| term == &lower_value)
.any(|term: &String| term == &value_as_string)
} else {
string_should_be_printed(pattern, &lower_value)
string_should_be_printed(pattern, &value_as_string)
}
}
Value::Glob { .. } | Value::CellPath { .. } | Value::Custom { .. } => {
string_should_be_printed(pattern, &lower_value)
string_should_be_printed(pattern, &value_as_string)
}
Value::String { val, .. } => string_should_be_printed(pattern, val),
Value::List { vals, .. } => vals
@ -597,7 +652,8 @@ pub fn find_internal(
let pattern = MatchPattern {
regex,
lower_terms: vec![search_term.to_lowercase()],
search_terms: vec![search_term.to_lowercase()],
ignore_case: true,
highlight,
invert: false,
string_style,

View File

@ -167,7 +167,7 @@ fn first_helper(
Err(ShellError::AccessEmptyContent { span: head })
}
} else {
Ok(PipelineData::ListStream(
Ok(PipelineData::list_stream(
stream.modify(|iter| iter.take(rows)),
metadata,
))
@ -191,7 +191,7 @@ fn first_helper(
}
} else {
// Just take 'rows' bytes off the stream, mimicking the binary behavior
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
ByteStream::read(
reader.take(rows as u64),
head,
@ -202,7 +202,7 @@ fn first_helper(
))
}
} else {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
} else {
Err(ShellError::OnlySupportsThisInputType {

View File

@ -45,6 +45,11 @@ If multiple cell paths are given, this will produce a list of values."#
"make all cell path members optional (returns `null` for missing values)",
Some('o'),
)
.switch(
"ignore-case",
"make all cell path members case insensitive",
None,
)
.switch(
"ignore-errors",
"ignore missing data (make all cell path members optional) (deprecated)",
@ -74,6 +79,30 @@ If multiple cell paths are given, this will produce a list of values."#
Span::test_data(),
)),
},
Example {
description: "Get a column from a table where some rows don't have that column, using optional cell-path syntax",
example: "[{A: A0, B: B0}, {B: B1}, {A: A2, B: B2}] | get A?",
result: Some(Value::list(
vec![
Value::test_string("A0"),
Value::test_nothing(),
Value::test_string("A2"),
],
Span::test_data(),
)),
},
Example {
description: "Get a column from a table where some rows don't have that column, using the optional flag",
example: "[{A: A0, B: B0}, {B: B1}, {A: A2, B: B2}] | get -o A",
result: Some(Value::list(
vec![
Value::test_string("A0"),
Value::test_nothing(),
Value::test_string("A2"),
],
Span::test_data(),
)),
},
Example {
description: "Get a cell from a table",
example: "[{A: A0}] | get 0.A",
@ -90,8 +119,13 @@ If multiple cell paths are given, this will produce a list of values."#
result: None,
},
Example {
description: "Getting Path/PATH in a case insensitive way",
example: "$env | get paTH!",
description: "Getting environment variables in a case insensitive way, using case insensitive cell-path syntax",
example: "$env | get home! path!",
result: None,
},
Example {
description: "Getting environment variables in a case insensitive way, using the '--ignore-case' flag",
example: "$env | get --ignore-case home path",
result: None,
},
Example {
@ -114,13 +148,16 @@ If multiple cell paths are given, this will produce a list of values."#
) -> Result<PipelineData, ShellError> {
let cell_path: CellPath = call.req_const(working_set, 0)?;
let rest: Vec<CellPath> = call.rest_const(working_set, 1)?;
let ignore_errors = call.has_flag_const(working_set, "ignore-errors")?;
let optional = call.has_flag_const(working_set, "optional")?
|| call.has_flag_const(working_set, "ignore-errors")?;
let ignore_case = call.has_flag_const(working_set, "ignore-case")?;
let metadata = input.metadata();
action(
input,
cell_path,
rest,
ignore_errors,
optional,
ignore_case,
working_set.permanent().signals().clone(),
call.head,
)
@ -138,12 +175,14 @@ If multiple cell paths are given, this will produce a list of values."#
let rest: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
let optional = call.has_flag(engine_state, stack, "optional")?
|| call.has_flag(engine_state, stack, "ignore-errors")?;
let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
let metadata = input.metadata();
action(
input,
cell_path,
rest,
optional,
ignore_case,
engine_state.signals().clone(),
call.head,
)
@ -175,6 +214,7 @@ fn action(
mut cell_path: CellPath,
mut rest: Vec<CellPath>,
optional: bool,
ignore_case: bool,
signals: Signals,
span: Span,
) -> Result<PipelineData, ShellError> {
@ -185,6 +225,13 @@ fn action(
}
}
if ignore_case {
cell_path.make_insensitive();
for path in &mut rest {
path.make_insensitive();
}
}
if let PipelineData::Empty = input {
return Err(ShellError::PipelineEmpty { dst_span: span });
}

View File

@ -264,7 +264,7 @@ fn insert(
value
}
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
} else {
let stream = stream.map(move |mut value| {
if let Err(e) = value.insert_data_at_cell_path(
@ -278,7 +278,7 @@ fn insert(
}
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
}
PipelineData::Empty => Err(ShellError::IncompatiblePathAccess {

View File

@ -120,7 +120,7 @@ interleave
.into_iter()
.chain(closures.into_iter().map(|closure| {
ClosureEvalOnce::new(engine_state, stack, closure)
.run_with_input(PipelineData::Empty)
.run_with_input(PipelineData::empty())
}))
.try_for_each(|stream| {
stream.and_then(|stream| {

View File

@ -42,7 +42,7 @@ impl Command for Items {
let metadata = input.metadata();
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(value, ..) => {
let span = value.span();
match value {
@ -55,7 +55,7 @@ impl Command for Items {
let result = closure
.add_arg(Value::string(col, span))
.add_arg(val)
.run_with_input(PipelineData::Empty)
.run_with_input(PipelineData::empty())
.and_then(|data| data.into_value(head));
match result {

View File

@ -85,7 +85,7 @@ impl Command for Join {
Value::String { val: r_on, .. },
) => {
let result = join(rows_1, rows_2, l_on, r_on, join_type, span);
Ok(PipelineData::Value(result, metadata))
Ok(PipelineData::value(result, metadata))
}
_ => Err(ShellError::UnsupportedInput {
msg: "(PipelineData<table>, table, string, string)".into(),

View File

@ -186,7 +186,7 @@ impl Command for Last {
}
}
} else {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
} else {
Err(ShellError::OnlySupportsThisInputType {

View File

@ -57,7 +57,7 @@ impl Command for Lines {
src_span: value.span(),
}),
},
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::ListStream(stream, metadata) => {
let stream = stream.modify(|iter| {
iter.filter_map(move |value| {
@ -81,7 +81,7 @@ impl Command for Lines {
.flatten()
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
PipelineData::ByteStream(stream, ..) => {
if let Some(lines) = stream.lines() {

View File

@ -130,7 +130,7 @@ impl Command for ParEach {
};
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(value, ..) => {
let span = value.span();
match value {

View File

@ -123,7 +123,7 @@ impl Command for Reduce {
acc = closure
.add_arg(value)
.add_arg(acc.clone())
.run_with_input(PipelineData::Value(acc, None))?
.run_with_input(PipelineData::value(acc, None))?
.into_value(head)?;
}

View File

@ -18,6 +18,11 @@ impl Command for Reject {
(Type::list(Type::Any), Type::list(Type::Any)),
])
.switch("optional", "make all cell path members optional", Some('o'))
.switch(
"ignore-case",
"make all cell path members case insensitive",
None,
)
.switch(
"ignore-errors",
"ignore missing data (make all cell path members optional) (deprecated)",
@ -93,12 +98,20 @@ impl Command for Reject {
let optional = call.has_flag(engine_state, stack, "optional")?
|| call.has_flag(engine_state, stack, "ignore-errors")?;
let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
if optional {
for cell_path in &mut new_columns {
cell_path.make_optional();
}
}
if ignore_case {
for cell_path in &mut new_columns {
cell_path.make_insensitive();
}
}
reject(engine_state, span, input, new_columns)
}

View File

@ -26,6 +26,11 @@ impl Command for Select {
"make all cell path members optional (returns `null` for missing values)",
Some('o'),
)
.switch(
"ignore-case",
"make all cell path members case insensitive",
None,
)
.switch(
"ignore-errors",
"ignore missing data (make all cell path members optional) (deprecated)",
@ -110,6 +115,7 @@ produce a table, a list will produce a list, and a record will produce a record.
}
let optional = call.has_flag(engine_state, stack, "optional")?
|| call.has_flag(engine_state, stack, "ignore-errors")?;
let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
let span = call.head;
if optional {
@ -118,6 +124,12 @@ produce a table, a list will produce a list, and a record will produce a record.
}
}
if ignore_case {
for cell_path in &mut new_columns {
cell_path.make_insensitive();
}
}
select(engine_state, span, new_columns, input)
}
@ -143,6 +155,18 @@ produce a table, a list will produce a list, and a record will produce a record.
"a" => Value::test_string("a")
})])),
},
Example {
description: "Select a column even if some rows are missing that column",
example: "[{a: a0 b: b0} {b: b1}] | select -o a",
result: Some(Value::test_list(vec![
Value::test_record(record! {
"a" => Value::test_string("a0")
}),
Value::test_record(record! {
"a" => Value::test_nothing()
}),
])),
},
Example {
description: "Select a field in a record",
example: "{a: a b: b} | select a",

View File

@ -94,7 +94,7 @@ impl Command for Skip {
PipelineData::ByteStream(stream, metadata) => {
if stream.type_().is_binary_coercible() {
let span = stream.span();
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
stream.skip(span, n as u64)?,
metadata,
))

View File

@ -81,7 +81,7 @@ impl Command for Slice {
};
if count == 0 {
Ok(PipelineData::Value(Value::list(vec![], head), None))
Ok(PipelineData::value(Value::list(vec![], head), None))
} else {
let iter = v.into_iter().skip(from).take(count);
Ok(iter.into_pipeline_data(head, engine_state.signals().clone()))
@ -102,7 +102,7 @@ impl Command for Slice {
};
if count == 0 {
Ok(PipelineData::Value(Value::list(vec![], head), None))
Ok(PipelineData::value(Value::list(vec![], head), None))
} else {
let iter = input.into_iter().skip(from).take(count);
Ok(iter.into_pipeline_data(head, engine_state.signals().clone()))

View File

@ -62,7 +62,7 @@ impl Command for Take {
)),
Value::Binary { val, .. } => {
let slice: Vec<u8> = val.into_iter().take(rows_desired).collect();
Ok(PipelineData::Value(Value::binary(slice, span), metadata))
Ok(PipelineData::value(Value::binary(slice, span), metadata))
}
Value::Range { val, .. } => Ok(val
.into_range_iter(span, Signals::empty())
@ -82,14 +82,14 @@ impl Command for Take {
}),
}
}
PipelineData::ListStream(stream, metadata) => Ok(PipelineData::ListStream(
PipelineData::ListStream(stream, metadata) => Ok(PipelineData::list_stream(
stream.modify(|iter| iter.take(rows_desired)),
metadata,
)),
PipelineData::ByteStream(stream, metadata) => {
if stream.type_().is_binary_coercible() {
let span = stream.span();
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
stream.take(span, rows_desired as u64)?,
metadata,
))

View File

@ -138,7 +138,7 @@ use it in your pipeline."#
let tee_thread = spawn_tee(info, eval_block)?;
let tee = IoTee::new(read, tee_thread);
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
ByteStream::read(tee, span, engine_state.signals().clone(), type_),
metadata,
))
@ -151,7 +151,7 @@ use it in your pipeline."#
let tee_thread = spawn_tee(info, eval_block)?;
let tee = IoTee::new(file, tee_thread);
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
ByteStream::read(tee, span, engine_state.signals().clone(), type_),
metadata,
))
@ -234,7 +234,7 @@ use it in your pipeline."#
};
if child.stdout.is_some() || child.stderr.is_some() {
Ok(PipelineData::ByteStream(
Ok(PipelineData::byte_stream(
ByteStream::child(*child, span),
metadata,
))
@ -243,7 +243,7 @@ use it in your pipeline."#
thread.join().unwrap_or_else(|_| Err(panic_error()))?;
}
child.wait()?;
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
}
}
@ -439,7 +439,7 @@ fn spawn_tee(
Signals::empty(),
info.type_,
);
eval_block(PipelineData::ByteStream(stream, info.metadata))
eval_block(PipelineData::byte_stream(stream, info.metadata))
})
.map_err(|err| {
IoError::new_with_additional_context(err, info.span, None, "Could not spawn tee")

View File

@ -293,7 +293,7 @@ pub fn transpose(
})
.collect::<Vec<Value>>();
if result_data.len() == 1 && args.as_record {
Ok(PipelineData::Value(
Ok(PipelineData::value(
result_data
.pop()
.expect("already check result only contains one item"),

View File

@ -210,7 +210,7 @@ fn update(
}
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
} else {
let stream = stream.map(move |mut value| {
if let Err(e) =
@ -222,7 +222,7 @@ fn update(
}
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
}
PipelineData::Empty => Err(ShellError::IncompatiblePathAccess {

View File

@ -288,7 +288,7 @@ fn upsert(
}
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
} else {
let stream = stream.map(move |mut value| {
if let Err(e) =
@ -300,7 +300,7 @@ fn upsert(
}
});
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
}
PipelineData::Empty => Err(ShellError::IncompatiblePathAccess {
@ -335,7 +335,7 @@ fn upsert_value_by_closure(
let input = value_at_path
.map(Cow::into_owned)
.map(IntoPipelineData::into_pipeline_data)
.unwrap_or(PipelineData::Empty);
.unwrap_or(PipelineData::empty());
let new_value = closure
.add_arg(arg)
@ -366,7 +366,7 @@ fn upsert_single_value_by_closure(
let input = value_at_path
.map(Cow::into_owned)
.map(IntoPipelineData::into_pipeline_data)
.unwrap_or(PipelineData::Empty);
.unwrap_or(PipelineData::empty());
let new_value = closure
.add_arg(arg)

View File

@ -137,7 +137,7 @@ fn values(
let signals = engine_state.signals().clone();
let metadata = input.metadata();
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(v, ..) => {
let span = v.span();
match v {

View File

@ -120,12 +120,12 @@ impl Command for Window {
PipelineData::Value(Value::List { vals, .. }, metadata) => {
let chunks = WindowGapIter::new(vals, size, stride, remainder, head);
let stream = ListStream::new(chunks, head, engine_state.signals().clone());
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
PipelineData::ListStream(stream, metadata) => {
let stream = stream
.modify(|iter| WindowGapIter::new(iter, size, stride, remainder, head));
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
input => Err(input.unsupported_input_error("list", head)),
}
@ -134,12 +134,12 @@ impl Command for Window {
PipelineData::Value(Value::List { vals, .. }, metadata) => {
let chunks = WindowOverlapIter::new(vals, size, stride, remainder, head);
let stream = ListStream::new(chunks, head, engine_state.signals().clone());
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
PipelineData::ListStream(stream, metadata) => {
let stream = stream
.modify(|iter| WindowOverlapIter::new(iter, size, stride, remainder, head));
Ok(PipelineData::ListStream(stream, metadata))
Ok(PipelineData::list_stream(stream, metadata))
}
input => Err(input.unsupported_input_error("list", head)),
}

View File

@ -36,7 +36,7 @@ impl Command for Wrap {
let metadata = input.metadata();
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(Value::Range { .. }, ..)
| PipelineData::Value(Value::List { .. }, ..)
| PipelineData::ListStream { .. } => Ok(input

View File

@ -103,7 +103,7 @@ impl Command for Zip {
let metadata = input.metadata();
let other = if let Value::Closure { val, .. } = other {
// If a closure was provided, evaluate it and consume its stream output
ClosureEvalOnce::new(engine_state, stack, *val).run_with_input(PipelineData::Empty)?
ClosureEvalOnce::new(engine_state, stack, *val).run_with_input(PipelineData::empty())?
} else {
other.into_pipeline_data()
};

View File

@ -95,11 +95,11 @@ pub(super) fn from_delimited_data(
) -> Result<PipelineData, ShellError> {
let metadata = input.metadata().map(|md| md.with_content_type(None));
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Empty => Ok(PipelineData::empty()),
PipelineData::Value(value, ..) => {
let string = value.into_string()?;
let byte_stream = ByteStream::read_string(string, name, Signals::empty());
Ok(PipelineData::ListStream(
Ok(PipelineData::list_stream(
from_delimited_stream(config, byte_stream, name)?,
metadata,
))
@ -110,7 +110,7 @@ pub(super) fn from_delimited_data(
dst_span: name,
src_span: list_stream.span(),
}),
PipelineData::ByteStream(byte_stream, ..) => Ok(PipelineData::ListStream(
PipelineData::ByteStream(byte_stream, ..) => Ok(PipelineData::list_stream(
from_delimited_stream(config, byte_stream, name)?,
metadata,
)),

View File

@ -76,25 +76,27 @@ impl Command for FromJson {
if call.has_flag(engine_state, stack, "objects")? {
// Return a stream of JSON values, one for each non-empty line
match input {
PipelineData::Value(Value::String { val, .. }, ..) => Ok(PipelineData::ListStream(
read_json_lines(
Cursor::new(val),
span,
strict,
engine_state.signals().clone(),
),
metadata,
)),
PipelineData::Value(Value::String { val, .. }, ..) => {
Ok(PipelineData::list_stream(
read_json_lines(
Cursor::new(val),
span,
strict,
engine_state.signals().clone(),
),
metadata,
))
}
PipelineData::ByteStream(stream, ..)
if stream.type_() != ByteStreamType::Binary =>
{
if let Some(reader) = stream.reader() {
Ok(PipelineData::ListStream(
Ok(PipelineData::list_stream(
read_json_lines(reader, span, strict, Signals::empty()),
metadata,
))
} else {
Ok(PipelineData::Empty)
Ok(PipelineData::empty())
}
}
_ => Err(ShellError::OnlySupportsThisInputType {
@ -184,7 +186,7 @@ fn convert_nujson_to_value(value: nu_json::Value, span: Span) -> Value {
}
}
fn convert_string_to_value(string_input: &str, span: Span) -> Result<Value, ShellError> {
pub(crate) fn convert_string_to_value(string_input: &str, span: Span) -> Result<Value, ShellError> {
match nu_json::from_str(string_input) {
Ok(value) => Ok(convert_nujson_to_value(value, span)),

View File

@ -27,3 +27,6 @@ pub use xlsx::FromXlsx;
pub use xml::FromXml;
pub use yaml::FromYaml;
pub use yaml::FromYml;
#[cfg(feature = "sqlite")]
pub(crate) use json::convert_string_to_value as convert_json_string_to_value;

View File

@ -168,7 +168,7 @@ fn from_ods(
}
}
Ok(PipelineData::Value(
Ok(PipelineData::value(
Value::record(dict.into_iter().collect(), head),
None,
))

View File

@ -181,7 +181,7 @@ fn from_xlsx(
}
}
Ok(PipelineData::Value(
Ok(PipelineData::value(
Value::record(dict.into_iter().collect(), head),
None,
))

View File

@ -132,7 +132,7 @@ pub fn to_delimited_data(
Value::Record { val, .. } => val.columns().cloned().collect(),
_ => return Err(make_unsupported_input_error(value.get_type(), head, span)),
};
input = PipelineData::Value(value, metadata.clone());
input = PipelineData::value(value, metadata.clone());
columns
}
};
@ -181,5 +181,5 @@ pub fn to_delimited_data(
},
);
Ok(PipelineData::ByteStream(stream, metadata))
Ok(PipelineData::byte_stream(stream, metadata))
}

View File

@ -76,7 +76,7 @@ impl Command for ToJson {
data_source: nu_protocol::DataSource::None,
content_type: Some(mime::APPLICATION_JSON.to_string()),
};
Ok(PipelineData::Value(res, Some(metadata)))
Ok(PipelineData::value(res, Some(metadata)))
}
_ => Err(ShellError::CantConvert {
to_type: "JSON".into(),

View File

@ -121,10 +121,10 @@ impl Command for ToText {
)
};
Ok(PipelineData::ByteStream(stream, update_metadata(meta)))
Ok(PipelineData::byte_stream(stream, update_metadata(meta)))
}
PipelineData::ByteStream(stream, meta) => {
Ok(PipelineData::ByteStream(stream, update_metadata(meta)))
Ok(PipelineData::byte_stream(stream, update_metadata(meta)))
}
}
}

View File

@ -112,7 +112,7 @@ In this case, generation also stops when the input stream stops."#
let closure_result = closure
.add_arg(state_arg)
.run_with_input(PipelineData::Empty);
.run_with_input(PipelineData::empty());
let (output, next_input) = parse_closure_result(closure_result, head);
// We use `state` to control when to stop, not `output`. By wrapping
@ -135,7 +135,7 @@ In this case, generation also stops when the input stream stops."#
let closure_result = closure
.add_arg(item)
.add_arg(state_arg)
.run_with_input(PipelineData::Empty);
.run_with_input(PipelineData::empty());
let (output, next_input) = parse_closure_result(closure_result, head);
state = next_input;
Some(output)

View File

@ -76,7 +76,7 @@ pub fn calculate(
PipelineData::Value(Value::List { ref vals, .. }, ..) => match &vals[..] {
[Value::Record { .. }, _end @ ..] => helper_for_tables(
vals,
values.span().expect("PipelineData::Value had no span"),
values.span().expect("PipelineData::value had no span"),
name,
mf,
),

View File

@ -1,26 +1,35 @@
use crate::{formats::value_to_json_value, network::tls::tls};
use crate::{
formats::value_to_json_value,
network::{http::timeout_extractor_reader::UreqTimeoutExtractorReader, tls::tls},
};
use base64::{
Engine, alphabet,
engine::{GeneralPurpose, general_purpose::PAD},
};
use http::StatusCode;
use log::error;
use multipart_rs::MultipartWriter;
use nu_engine::command_prelude::*;
use nu_protocol::{ByteStream, LabeledError, Signals, shell_error::io::IoError};
use serde_json::Value as JsonValue;
use std::{
collections::HashMap,
error::Error as StdError,
io::Cursor,
path::PathBuf,
str::FromStr,
sync::mpsc::{self, RecvTimeoutError},
time::Duration,
};
use ureq::{Error, ErrorKind, Request, Response};
use ureq::{
Body, Error, RequestBuilder, ResponseExt, SendBody,
typestate::{WithBody, WithoutBody},
};
use url::Url;
const HTTP_DOCS: &str = "https://www.nushell.sh/cookbook/http.html";
type Response = http::Response<Body>;
type ContentType = String;
#[derive(Debug, PartialEq, Eq)]
@ -43,6 +52,20 @@ impl From<Option<ContentType>> for BodyType {
}
}
trait GetHeader {
fn header(&self, key: &str) -> Option<&str>;
}
impl GetHeader for Response {
fn header(&self, key: &str) -> Option<&str> {
self.headers().get(key).and_then(|v| {
v.to_str()
.map_err(|e| log::error!("Invalid header {e:?}"))
.ok()
})
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum RedirectMode {
Follow,
@ -56,21 +79,24 @@ pub fn http_client(
engine_state: &EngineState,
stack: &mut Stack,
) -> Result<ureq::Agent, ShellError> {
let mut agent_builder = ureq::builder()
let mut config_builder = ureq::config::Config::builder()
.user_agent("nushell")
.tls_connector(std::sync::Arc::new(tls(allow_insecure)?));
.save_redirect_history(true)
.http_status_as_error(false)
.max_redirects_will_error(false);
if let RedirectMode::Manual | RedirectMode::Error = redirect_mode {
agent_builder = agent_builder.redirects(0);
config_builder = config_builder.max_redirects(0);
}
if let Some(http_proxy) = retrieve_http_proxy_from_env(engine_state, stack) {
if let Ok(proxy) = ureq::Proxy::new(http_proxy) {
agent_builder = agent_builder.proxy(proxy);
if let Ok(proxy) = ureq::Proxy::new(&http_proxy) {
config_builder = config_builder.proxy(Some(proxy));
}
};
Ok(agent_builder.build())
config_builder = config_builder.tls_config(tls(allow_insecure)?);
Ok(ureq::Agent::new_with_config(config_builder.build()))
}
pub fn http_parse_url(
@ -92,7 +118,7 @@ pub fn http_parse_url(
msg: "Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com".to_string(),
input: format!("value: '{requested_url:?}'"),
msg_span: call.head,
input_span: span
input_span: span,
});
}
};
@ -141,20 +167,22 @@ pub fn response_to_buffer(
_ => ByteStreamType::Unknown,
};
let reader = response.into_reader();
let reader = UreqTimeoutExtractorReader {
r: response.into_body().into_reader(),
};
PipelineData::ByteStream(
PipelineData::byte_stream(
ByteStream::read(reader, span, engine_state.signals().clone(), response_type)
.with_known_size(buffer_size),
None,
)
}
pub fn request_add_authorization_header(
pub fn request_add_authorization_header<B>(
user: Option<String>,
password: Option<String>,
mut request: Request,
) -> Request {
mut request: RequestBuilder<B>,
) -> RequestBuilder<B> {
let base64_engine = GeneralPurpose::new(&alphabet::STANDARD, PAD);
let login = match (user, password) {
@ -177,7 +205,7 @@ pub fn request_add_authorization_header(
};
if let Some(login) = login {
request = request.set("Authorization", &format!("Basic {login}"));
request = request.header("Authorization", &format!("Basic {login}"));
}
request
@ -200,34 +228,45 @@ impl From<ShellError> for ShellErrorOrRequestError {
pub enum HttpBody {
Value(Value),
ByteStream(ByteStream),
None,
}
pub fn send_request_no_body(
request: RequestBuilder<WithoutBody>,
span: Span,
signals: &Signals,
) -> (Result<Response, ShellError>, Headers) {
let headers = extract_request_headers(&request);
let request_url = request.uri_ref().cloned().unwrap_or_default().to_string();
let result = send_cancellable_request(&request_url, Box::new(|| request.call()), span, signals)
.map_err(|e| request_error_to_shell_error(span, e));
(result, headers.unwrap_or_default())
}
// remove once all commands have been migrated
pub fn send_request(
engine_state: &EngineState,
request: Request,
http_body: HttpBody,
request: RequestBuilder<WithBody>,
body: HttpBody,
content_type: Option<String>,
span: Span,
signals: &Signals,
) -> Result<Response, ShellErrorOrRequestError> {
let request_url = request.url().to_string();
) -> (Result<Response, ShellError>, Headers) {
let mut request_headers = Headers::new();
let request_url = request.uri_ref().cloned().unwrap_or_default().to_string();
// hard code serialze_types to false because closures probably shouldn't be
// deserialized for send_request but it's required by send_json_request
let serialze_types = false;
match http_body {
HttpBody::None => {
send_cancellable_request(&request_url, Box::new(|| request.call()), span, signals)
}
let response = match body {
HttpBody::ByteStream(byte_stream) => {
let req = if let Some(content_type) = content_type {
request.set("Content-Type", &content_type)
request.header("Content-Type", &content_type)
} else {
request
};
if let Some(h) = extract_request_headers(&req) {
request_headers = h;
}
send_cancellable_request_bytes(&request_url, req, byte_stream, span, signals)
}
HttpBody::Value(body) => {
@ -236,11 +275,15 @@ pub fn send_request(
// We should set the content_type if there is one available
// when the content type is unknown
let req = if let BodyType::Unknown(Some(content_type)) = &body_type {
request.clone().set("Content-Type", content_type)
request.header("Content-Type", content_type)
} else {
request
};
if let Some(h) = extract_request_headers(&req) {
request_headers = h;
}
match body_type {
BodyType::Json => send_json_request(
engine_state,
@ -260,14 +303,18 @@ pub fn send_request(
}
}
}
}
};
let response = response.map_err(|e| request_error_to_shell_error(span, e));
(response, request_headers)
}
fn send_json_request(
engine_state: &EngineState,
request_url: &str,
body: Value,
req: Request,
req: RequestBuilder<WithBody>,
span: Span,
signals: &Signals,
serialize_types: bool,
@ -311,7 +358,7 @@ fn send_json_request(
fn send_form_request(
request_url: &str,
body: Value,
req: Request,
req: RequestBuilder<WithBody>,
span: Span,
signals: &Signals,
) -> Result<Response, ShellErrorOrRequestError> {
@ -321,7 +368,7 @@ fn send_form_request(
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect::<Vec<(&str, &str)>>();
req.send_form(&data)
req.send_form(data)
};
match body {
@ -364,7 +411,7 @@ fn send_form_request(
fn send_multipart_request(
request_url: &str,
body: Value,
req: Request,
req: RequestBuilder<WithBody>,
span: Span,
signals: &Signals,
) -> Result<Response, ShellErrorOrRequestError> {
@ -401,7 +448,7 @@ fn send_multipart_request(
let (boundary, data) = (builder.boundary, builder.data);
let content_type = format!("multipart/form-data; boundary={boundary}");
move || req.set("Content-Type", &content_type).send_bytes(&data)
move || req.header("Content-Type", &content_type).send(&data)
}
_ => {
return Err(ShellErrorOrRequestError::ShellError(
@ -418,23 +465,17 @@ fn send_multipart_request(
fn send_default_request(
request_url: &str,
body: Value,
req: Request,
req: RequestBuilder<WithBody>,
span: Span,
signals: &Signals,
) -> Result<Response, ShellErrorOrRequestError> {
match body {
Value::Binary { val, .. } => send_cancellable_request(
request_url,
Box::new(move || req.send_bytes(&val)),
span,
signals,
),
Value::String { val, .. } => send_cancellable_request(
request_url,
Box::new(move || req.send_string(&val)),
span,
signals,
),
Value::Binary { val, .. } => {
send_cancellable_request(request_url, Box::new(move || req.send(&val)), span, signals)
}
Value::String { val, .. } => {
send_cancellable_request(request_url, Box::new(move || req.send(&val)), span, signals)
}
_ => Err(ShellErrorOrRequestError::ShellError(
ShellError::TypeMismatch {
err_message: format!("Accepted types: [binary, string]. Check: {HTTP_DOCS}"),
@ -487,7 +528,7 @@ fn send_cancellable_request(
// ureq functions can block for a long time (default 30s?) while attempting to make an HTTP connection
fn send_cancellable_request_bytes(
request_url: &str,
request: Request,
request: ureq::RequestBuilder<WithBody>,
byte_stream: ByteStream,
span: Span,
signals: &Signals,
@ -511,9 +552,11 @@ fn send_cancellable_request_bytes(
})
})
.and_then(|reader| {
request.send(reader).map_err(|e| {
ShellErrorOrRequestError::RequestError(request_url_string, Box::new(e))
})
request
.send(SendBody::from_owned_reader(reader))
.map_err(|e| {
ShellErrorOrRequestError::RequestError(request_url_string, Box::new(e))
})
});
// may fail if the user has cancelled the operation
@ -537,10 +580,10 @@ fn send_cancellable_request_bytes(
}
}
pub fn request_set_timeout(
pub fn request_set_timeout<B>(
timeout: Option<Value>,
mut request: Request,
) -> Result<Request, ShellError> {
mut request: RequestBuilder<B>,
) -> Result<RequestBuilder<B>, ShellError> {
if let Some(timeout) = timeout {
let val = timeout.as_duration()?;
if val.is_negative() || val < 1 {
@ -550,16 +593,19 @@ pub fn request_set_timeout(
});
}
request = request.timeout(Duration::from_nanos(val as u64));
request = request
.config()
.timeout_global(Some(Duration::from_nanos(val as u64)))
.build()
}
Ok(request)
}
pub fn request_add_custom_headers(
pub fn request_add_custom_headers<B>(
headers: Option<Value>,
mut request: Request,
) -> Result<Request, ShellError> {
mut request: RequestBuilder<B>,
) -> Result<RequestBuilder<B>, ShellError> {
if let Some(headers) = headers {
let mut custom_headers: HashMap<String, Value> = HashMap::new();
@ -611,7 +657,7 @@ pub fn request_add_custom_headers(
for (k, v) in custom_headers {
if let Ok(s) = v.coerce_into_string() {
request = request.set(&k, &s);
request = request.header(&k, &s);
}
}
}
@ -619,63 +665,57 @@ pub fn request_add_custom_headers(
Ok(request)
}
fn handle_response_error(span: Span, requested_url: &str, response_err: Error) -> ShellError {
match response_err {
Error::Status(301, _) => ShellError::NetworkFailure {
fn handle_status_error(span: Span, requested_url: &str, status: StatusCode) -> ShellError {
match status {
StatusCode::MOVED_PERMANENTLY => ShellError::NetworkFailure {
msg: format!("Resource moved permanently (301): {requested_url:?}"),
span,
},
Error::Status(400, _) => ShellError::NetworkFailure {
StatusCode::BAD_REQUEST => ShellError::NetworkFailure {
msg: format!("Bad request (400) to {requested_url:?}"),
span,
},
Error::Status(403, _) => ShellError::NetworkFailure {
StatusCode::FORBIDDEN => ShellError::NetworkFailure {
msg: format!("Access forbidden (403) to {requested_url:?}"),
span,
},
Error::Status(404, _) => ShellError::NetworkFailure {
StatusCode::NOT_FOUND => ShellError::NetworkFailure {
msg: format!("Requested file not found (404): {requested_url:?}"),
span,
},
Error::Status(408, _) => ShellError::NetworkFailure {
StatusCode::REQUEST_TIMEOUT => ShellError::NetworkFailure {
msg: format!("Request timeout (408): {requested_url:?}"),
span,
},
Error::Status(_, _) => ShellError::NetworkFailure {
c => ShellError::NetworkFailure {
msg: format!(
"Cannot make request to {:?}. Error is {:?}",
requested_url,
response_err.to_string()
c.to_string()
),
span,
},
}
}
Error::Transport(t) => {
let generic_network_failure = || ShellError::NetworkFailure {
msg: t.to_string(),
span,
};
match t.kind() {
ErrorKind::ConnectionFailed => ShellError::NetworkFailure {
msg: format!(
"Cannot make request to {requested_url}, there was an error establishing a connection.",
),
span,
},
ErrorKind::Io => 'io: {
let Some(source) = t.source() else {
break 'io generic_network_failure();
};
let Some(io_error) = source.downcast_ref::<std::io::Error>() else {
break 'io generic_network_failure();
};
ShellError::Io(IoError::new(io_error, span, None))
}
_ => generic_network_failure(),
}
}
fn handle_response_error(span: Span, requested_url: &str, response_err: Error) -> ShellError {
match response_err {
Error::ConnectionFailed => ShellError::NetworkFailure {
msg: format!(
"Cannot make request to {requested_url}, there was an error establishing a connection.",
),
span,
},
Error::Timeout(..) => ShellError::Io(IoError::new(
ErrorKind::from_std(std::io::ErrorKind::TimedOut),
span,
None,
)),
Error::Io(error) => ShellError::Io(IoError::new(error, span, None)),
e => ShellError::NetworkFailure {
msg: e.to_string(),
span,
},
}
}
@ -743,31 +783,60 @@ fn transform_response_using_content_type(
pub fn check_response_redirection(
redirect_mode: RedirectMode,
span: Span,
response: &Result<Response, ShellErrorOrRequestError>,
resp: &Response,
) -> Result<(), ShellError> {
if let Ok(resp) = response {
if RedirectMode::Error == redirect_mode && (300..400).contains(&resp.status()) {
return Err(ShellError::NetworkFailure {
msg: format!(
"Redirect encountered when redirect handling mode was 'error' ({} {})",
resp.status(),
resp.status_text()
),
span,
});
}
if RedirectMode::Error == redirect_mode && (300..400).contains(&resp.status().as_u16()) {
return Err(ShellError::NetworkFailure {
msg: format!(
"Redirect encountered when redirect handling mode was 'error' ({})",
resp.status()
),
span,
});
}
Ok(())
}
fn request_handle_response_content(
pub(crate) fn handle_response_status(
resp: &Response,
redirect_mode: RedirectMode,
requested_url: &str,
span: Span,
allow_errors: bool,
) -> Result<(), ShellError> {
let manual_redirect = redirect_mode == RedirectMode::Manual;
let is_success = resp.status().is_success()
|| allow_errors
|| (resp.status().is_redirection() && manual_redirect);
if is_success {
Ok(())
} else {
Err(handle_status_error(span, requested_url, resp.status()))
}
}
pub(crate) struct RequestMetadata<'a> {
pub requested_url: &'a str,
pub span: Span,
pub headers: Headers,
pub redirect_mode: RedirectMode,
pub flags: RequestFlags,
}
pub(crate) fn request_handle_response(
engine_state: &EngineState,
stack: &mut Stack,
span: Span,
requested_url: &str,
flags: RequestFlags,
RequestMetadata {
requested_url,
span,
headers,
redirect_mode,
flags,
}: RequestMetadata,
resp: Response,
request: Request,
) -> Result<PipelineData, ShellError> {
// #response_to_buffer moves "resp" making it impossible to read headers later.
// Wrapping it into a closure to call when needed
@ -787,11 +856,18 @@ fn request_handle_response_content(
None => Ok(response_to_buffer(response, engine_state, span)),
}
};
handle_response_status(
&resp,
redirect_mode,
requested_url,
span,
flags.allow_errors,
)?;
if flags.full {
let response_status = resp.status();
let request_headers_value = headers_to_nu(&extract_request_headers(&request), span)
let request_headers_value = headers_to_nu(&headers, span)
.and_then(|data| data.into_value(span))
.unwrap_or(Value::nothing(span));
@ -803,14 +879,23 @@ fn request_handle_response_content(
"request" => request_headers_value,
"response" => response_headers_value,
};
let urls = Value::list(
resp.get_redirect_history()
.into_iter()
.flatten()
.map(|v| Value::string(v.to_string(), span))
.collect(),
span,
);
let body = consume_response_body(resp)?.into_value(span)?;
let full_response = Value::record(
record! {
"urls" => urls,
"headers" => Value::record(headers, span),
"body" => body,
"status" => Value::int(response_status as i64, span),
"status" => Value::int(response_status.as_u16().into(), span),
},
span,
);
@ -821,79 +906,58 @@ fn request_handle_response_content(
}
}
pub fn request_handle_response(
engine_state: &EngineState,
stack: &mut Stack,
span: Span,
requested_url: &str,
flags: RequestFlags,
response: Result<Response, ShellErrorOrRequestError>,
request: Request,
) -> Result<PipelineData, ShellError> {
match response {
Ok(resp) => request_handle_response_content(
engine_state,
stack,
span,
requested_url,
flags,
resp,
request,
),
Err(e) => match e {
ShellErrorOrRequestError::ShellError(e) => Err(e),
ShellErrorOrRequestError::RequestError(_, e) => {
if flags.allow_errors {
if let Error::Status(_, resp) = *e {
Ok(request_handle_response_content(
engine_state,
stack,
span,
requested_url,
flags,
resp,
request,
)?)
} else {
Err(handle_response_error(span, requested_url, *e))
}
} else {
Err(handle_response_error(span, requested_url, *e))
}
}
},
}
}
type Headers = HashMap<String, Vec<String>>;
fn extract_request_headers(request: &Request) -> Headers {
request
.header_names()
.iter()
fn extract_request_headers<B>(request: &RequestBuilder<B>) -> Option<Headers> {
let headers = request.headers_ref()?;
let headers_str = headers
.keys()
.map(|name| {
(
name.clone(),
request.all(name).iter().map(|e| e.to_string()).collect(),
name.to_string().clone(),
headers
.get_all(name)
.iter()
.filter_map(|v| {
v.to_str()
.map_err(|e| {
error!("Invalid header {name:?}: {e:?}");
})
.ok()
.map(|s| s.to_string())
})
.collect(),
)
})
.collect();
Some(headers_str)
}
pub(crate) fn extract_response_headers(response: &Response) -> Headers {
let header_map = response.headers();
header_map
.keys()
.map(|name| {
(
name.to_string().clone(),
header_map
.get_all(name)
.iter()
.filter_map(|v| {
v.to_str()
.map_err(|e| {
error!("Invalid header {name:?}: {e:?}");
})
.ok()
.map(|s| s.to_string())
})
.collect(),
)
})
.collect()
}
fn extract_response_headers(response: &Response) -> Headers {
response
.headers_names()
.iter()
.map(|name| {
(
name.clone(),
response.all(name).iter().map(|e| e.to_string()).collect(),
)
})
.collect()
}
fn headers_to_nu(headers: &Headers, span: Span) -> Result<PipelineData, ShellError> {
pub(crate) fn headers_to_nu(headers: &Headers, span: Span) -> Result<PipelineData, ShellError> {
let mut vals = Vec::with_capacity(headers.len());
for (name, values) in headers {
@ -927,18 +991,12 @@ fn headers_to_nu(headers: &Headers, span: Span) -> Result<PipelineData, ShellErr
Ok(Value::list(vals, span).into_pipeline_data())
}
pub fn request_handle_response_headers(
span: Span,
response: Result<Response, ShellErrorOrRequestError>,
) -> Result<PipelineData, ShellError> {
match response {
Ok(resp) => headers_to_nu(&extract_response_headers(&resp), span),
Err(e) => match e {
ShellErrorOrRequestError::ShellError(e) => Err(e),
ShellErrorOrRequestError::RequestError(requested_url, e) => {
Err(handle_response_error(span, &requested_url, *e))
}
},
pub(crate) fn request_error_to_shell_error(span: Span, e: ShellErrorOrRequestError) -> ShellError {
match e {
ShellErrorOrRequestError::ShellError(e) => e,
ShellErrorOrRequestError::RequestError(requested_url, e) => {
handle_response_error(span, &requested_url, *e)
}
}
}

View File

@ -1,7 +1,8 @@
use crate::network::http::client::{
HttpBody, RequestFlags, check_response_redirection, http_client, http_parse_redirect_mode,
http_parse_url, request_add_authorization_header, request_add_custom_headers,
request_handle_response, request_set_timeout, send_request,
HttpBody, RequestFlags, RequestMetadata, check_response_redirection, http_client,
http_parse_redirect_mode, http_parse_url, request_add_authorization_header,
request_add_custom_headers, request_handle_response, request_set_timeout, send_request,
send_request_no_body,
};
use nu_engine::command_prelude::*;
@ -148,7 +149,7 @@ impl Command for HttpDelete {
struct Arguments {
url: Value,
headers: Option<Value>,
data: HttpBody,
data: Option<HttpBody>,
content_type: Option<String>,
raw: bool,
insecure: bool,
@ -168,13 +169,13 @@ fn run_delete(
) -> Result<PipelineData, ShellError> {
let (data, maybe_metadata) = call
.get_flag::<Value>(engine_state, stack, "data")?
.map(|v| (HttpBody::Value(v), None))
.map(|v| (Some(HttpBody::Value(v)), None))
.unwrap_or_else(|| match input {
PipelineData::Value(v, metadata) => (HttpBody::Value(v), metadata),
PipelineData::Value(v, metadata) => (Some(HttpBody::Value(v)), metadata),
PipelineData::ByteStream(byte_stream, metadata) => {
(HttpBody::ByteStream(byte_stream), metadata)
(Some(HttpBody::ByteStream(byte_stream)), metadata)
}
_ => (HttpBody::None, None),
_ => (None, None),
});
let content_type = call
.get_flag(engine_state, stack, "content-type")?
@ -216,31 +217,43 @@ fn helper(
request = request_set_timeout(args.timeout, request)?;
request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?;
let (response, request_headers) = match args.data {
None => send_request_no_body(request, call.head, engine_state.signals()),
let response = send_request(
engine_state,
request.clone(),
args.data,
args.content_type,
call.head,
engine_state.signals(),
);
Some(body) => send_request(
engine_state,
// Nushell allows sending body via delete method, but not via get.
// We should probably unify the behaviour here.
//
// Sending body with DELETE goes against the spec, but might be useful in some cases,
// see [force_send_body] documentation.
request.force_send_body(),
body,
args.content_type,
span,
engine_state.signals(),
),
};
let request_flags = RequestFlags {
raw: args.raw,
full: args.full,
allow_errors: args.allow_errors,
};
let response = response?;
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response(
engine_state,
stack,
span,
&requested_url,
request_flags,
RequestMetadata {
requested_url: &requested_url,
span,
headers: request_headers,
redirect_mode,
flags: request_flags,
},
response,
request,
)
}

View File

@ -1,12 +1,10 @@
use crate::network::http::client::{
RequestFlags, check_response_redirection, http_client, http_parse_redirect_mode,
http_parse_url, request_add_authorization_header, request_add_custom_headers,
request_handle_response, request_set_timeout, send_request,
RequestFlags, RequestMetadata, check_response_redirection, http_client,
http_parse_redirect_mode, http_parse_url, request_add_authorization_header,
request_add_custom_headers, request_handle_response, request_set_timeout, send_request_no_body,
};
use nu_engine::command_prelude::*;
use super::client::HttpBody;
#[derive(Clone)]
pub struct HttpGet;
@ -180,15 +178,8 @@ fn helper(
request = request_set_timeout(args.timeout, request)?;
request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?;
let response = send_request(
engine_state,
request.clone(),
HttpBody::None,
None,
call.head,
engine_state.signals(),
);
let (response, request_headers) =
send_request_no_body(request, call.head, engine_state.signals());
let request_flags = RequestFlags {
raw: args.raw,
@ -196,15 +187,20 @@ fn helper(
allow_errors: args.allow_errors,
};
let response = response?;
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response(
engine_state,
stack,
span,
&requested_url,
request_flags,
RequestMetadata {
requested_url: &requested_url,
span,
headers: request_headers,
redirect_mode,
flags: request_flags,
},
response,
request,
)
}

View File

@ -1,8 +1,7 @@
use super::client::HttpBody;
use crate::network::http::client::{
check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url,
request_add_authorization_header, request_add_custom_headers, request_handle_response_headers,
request_set_timeout, send_request,
check_response_redirection, extract_response_headers, handle_response_status, headers_to_nu,
http_client, http_parse_redirect_mode, http_parse_url, request_add_authorization_header,
request_add_custom_headers, request_set_timeout, send_request_no_body,
};
use nu_engine::command_prelude::*;
use nu_protocol::Signals;
@ -140,7 +139,6 @@ fn run_head(
}
// Helper function that actually goes to retrieve the resource from the url given
// The Option<String> return a possible file extension which can be used in AutoConvert commands
fn helper(
engine_state: &EngineState,
stack: &mut Stack,
@ -159,16 +157,11 @@ fn helper(
request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?;
let response = send_request(
engine_state,
request,
HttpBody::None,
None,
call.head,
signals,
);
let (response, _request_headers) = send_request_no_body(request, call.head, signals);
let response = response?;
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response_headers(span, response)
handle_response_status(&response, redirect_mode, &requested_url, span, false)?;
headers_to_nu(&extract_response_headers(&response), span)
}
#[cfg(test)]

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