Nushell 0.92.0

Nushell, or Nu for short, is a new shell that takes a modern, structured approach to your command line. It works seamlessly with the data from your filesystem, operating system, and a growing number of file formats to make it easy to build powerful command line pipelines.

Today, we're releasing version 0.92.0 of Nu. This release adds exciting new plugin features (persistence, a reworked API, and more functionality), a simple profiler, support for XDG_CONFIG_HOME, scoped file redirections, and changes to external command output.

Where to get it

Nu 0.92.0 is available as pre-built binariesopen in new window or from crates.ioopen in new window. If you have Rust installed you can install it using cargo install nu.


The optional dataframe functionality is available by cargo install nu --features=dataframe.

As part of this release, we also publish a set of optional plugins you can install and use with Nu. To install, use cargo install nu_plugin_<plugin name>.

Table of content

Themes of this release / New features [toc]

External command output changes [toc]

Breaking change

See a full overview of the breaking changes

With #11934open in new window, we improved the performance of pipelines with external commands. As part of this effort, Nushell now streams external command output in more places where it is possible. Namely, parentheses no longer collect external command output and are now solely for grouping and precedence. For example, take this pipeline:

(nu -c "for x in 1.. { try { print $x } catch { break } }")
| lines
| first

In previous versions, this would collect the output of nu and would not print anything until ctrl+c was pressed. Now, this will immediately print the first line and then immediately finish the pipeline.

So now, external command output will only be collected if it's being turned into a value (or if it's passed to an internal command that collects its input). For example, external commands will be collected if it's a command argument:

print (^external)

Or, if it's being stored in record, list, table, or variable:

    result: (^external)


let result = ^external

"Being turned into a value" now also includes closures in many cases. For example, each, insert, reduce, and many other commands run closures to compute values. In these cases, if an external command is in "return position" (it's the last command), then its output will be collected into a value instead of writing to the terminal/stdio. For example, the code below used to print "text" twice and then return an empty list:

1..2 | each { nu -c 'print text' }

But now, this will collect the output of nu, giving the list ['text', 'text']. To achieve the old behavior, you can return null instead:

1..2 | each { nu -c "print text"; null }
# or, you could use a `for` loop

Only a few commands take a closure but do not use it to compute a value:

  • do
  • with-env
  • collect
  • tee
  • watch

These commands will not collect external command output from the closure and will instead forward it to the next command in the pipeline or to the terminal/stdio.

Another notable change is that external command output will no longer be implicitly ignored. For example, external commands in subexpressions that were not the last command used to have their output discarded:

(^echo first; ^echo last)

Before, this would only print last, but now this prints both first and last.

One final change to note is that external command output now has trailing new lines removed by default. The exceptions are if the external command is being redirected to a file, another external command, to the terminal, or to the complete command.

Scoped file redirections [toc]

Breaking change

See a full overview of the breaking changes

File redirections (o>, e>, o+e>, etc.) now apply to all external commands inside an expression. For example, the snippet below will redirect stderr from both commands into err.log.

(nu -c "print -e first"; nu -c "print -e last") e> err.log
# err.log will contain: "first\nlast\n"

Note that if there were any custom commands called inside the subexpression, then any external commands inside the custom command would also use the same file redirection.

def cmd [] {

(^extern3; cmd) o> out.txt

# output from `extern1`, `extern2`, `extern3` will be redirected to the file

Tilde expansion [toc]

Breaking change

See a full overview of the breaking changes

Building off of last release's work with globs, this version makes similar changes to tilde expansion in #12232open in new window. Now, tildes will be expanded to the home directory only if it's part of a glob.

ls ~/dir              # expands tilde
ls "~/dir"            # does not expand tilde

let f = "~/dir"
ls $f                 # does not expand tilde
ls ($f | path expand) # tilde explicitly expanded
ls ($f | into glob)   # tilde will be expanded

let f: glob = "~/aaa"
ls $f                 # tilde will be expanded

Support for XDG_CONFIG_HOME [toc]

Breaking change

See a full overview of the breaking changes

When nushell firsts starts up, if it detects that the XDG_CONFIG_HOME environment variable is set to an absolute path, then nushell will use XDG_CONFIG_HOME/nushell as the config directory. Otherwise, nushell will use the same config directory as it did in previous versions. Note that setting XDG_CONFIG_HOME in will not work! XDG_CONFIG_HOME must be set before nushell is launched. This can be accomplished through settings in your terminal emulator, using another shell to launch nushell, by setting environment variables at the OS level, etc. The relevant PR is #12118open in new window.

Incorporating the extra feature by default [toc]

Breaking change

See a full overview of the breaking changes

The extra cargo feature was removed in this version, since all of its features and commands are now included by default with #12140open in new window. Packagers and users that compile nushell from source will need to check whether they enable the extra feature and remove it from their build scripts.

This was done to simplify the distribution of Nushell to have a canonical set of commands. Commands that we deem to serve a niche role or are not fully developed yet will now most likely be removed from the core nu binary and move into their dedicated plugins.

(Now only the dataframe feature remains to add a major set of commands, but work is underway to allow you to add this simply as a plugin)

If your current build of nushell does not have the extra feature enabled, then you should have access to more commands in this new release:

  • update cells provides an easy way to update all cells of a table using a closure.
  • each while is like the each command, except it stops once a null value is returned by the closure.
  • into bits returns the stringified binary representation of a value.
  • The bits family of commands which has bit-wise operations like bits and, bits or, bits not, bits shl, and more!
  • More math commands like math exp, math ln, and trigonometric functions like math sin, math sinh, math arcsin, and math arcsinh.
  • format pattern which provides a simpiler way to format columns of a table compared to, for example, using string interpolation.
  • fmt which formats numbers into binary, hex, octal, exponential notation, and more.
  • encode hex and decode hex allows one to encode and decode binary values as hex strings.
  • Commands to change the casing of strings like str camel-case, str kebab-case, and more.
  • The roll family of commands to roll columns or rows of a table up, down, left, or right.
  • The rotate command to rotate a table clockwise or counter clockwise.
  • ansi gradient adds a color gradient to text foreground and/or background.
  • to html converts a table into HTML text.
  • from url parses a url encoded string into a record.

Future releases may choose to remove some of those commands to plugins to slim down the nu binary or make sure that we can maintain stability guarantees after the 1.0 release of Nushell.

Persistent plugins [toc]


We recommend removing your file (from the config directory) when migrating to this new version, due to the significant changes made. You will then need register all of your plugins again after they have been updated.

A major enhancement for plugin users: plugins can now run persistently in the background and serve multiple calls before exiting! 🎉

This improves the performance of plugin commands considerably, because starting a process has a much more considerable overhead than talking to an existing one:

# 0.91.0
> 1..1000 | each { timeit { "2.3.2" | inc -m } } | math avg
2ms 498µs 493ns

# 0.92.0 (8x faster!)
> 1..1000 | each { timeit { "2.3.2" | inc -m } } | math avg
308µs 577ns

That difference is even more significant for plugins written in JIT or interpreted languages:

# 0.91.0
> 1..100 | each { timeit { nu-python 1 foo } } | math avg
40ms 704µs 902ns

# 0.92.0 (47x faster!)
> 1..1000 | each { timeit { nu-python 1 foo } } | math avg
871µs 410ns

This will open the door to plugins that would have otherwise been too slow to be useful, particularly in languages that have a reputation for being slow to cold start, like Java and other JVM languages.

By default, plugins will stay running for 10 seconds after they were last used, and quit if there is no activity. This behavior is configurable:

$env.config.plugin_gc = {
    # Settings for plugins not otherwise specified:
    default: {
        enabled: true # set to false to never automatically stop plugins
        stop_after: 10sec # how long to wait after the plugin is inactive before stopping it
    # Settings for specific plugins, by plugin name
    # (i.e. what you see in `plugin list`):
    plugins: {
        gstat: {
            stop_after: 1min
        inc: {
            stop_after: 0sec # stop as soon as possible
        stream_example: {
            enabled: false # never stop automatically

Aside from performance, this also enables plugins to have much more advanced behavior. Plugins can now maintain state across plugin calls, and for example use a handle custom value to track resources that remain in the plugin's process without having to be serialized somehow, or implement caches to speed up certain operations that are repeated often.

For more details on how to manage plugins with this change, see the newly added plugin list and plugin stop commands.

Plugin API overhaul [toc]

Breaking change

See a full overview of the breaking changes


Plugins are no longer launched in the current working directory of the shell. Instead, they are launched in the directory of their plugin executable. See this section of the plugins guide for details.

This release brings major reorganization to the plugin API. The Pluginopen in new window trait now specifies a list of PluginCommandopen in new window trait objects, which implement both the signature and the functionality for that command. Dispatching the commands by name is now handled by serve_plugin() automatically, so no more match statements! This should make creating plugins that expose many commands much easier, and also makes a one-command-per-module pattern easier to follow.

StreamingPlugin has been removed. Instead, PluginCommand uses the streaming API, with PipelineData input and output, and SimplePluginCommand has value input and output just as before.

The signature() method has been broken up into more methods to reflect the internal Command trait. This makes core commands and plugin commands look more similar to each other, and makes it easier to move core commands to plugins if we want to. The new methods are: name(), usage(), extra_usage(), examples(), search_terms(). PluginSignature and PluginExample are both no longer needed to be used by plugin developers - just use Signature and Example like core commands do.

The arguments passed to run() have also changed. It now takes &self rather than &mut self, and all plugins and commands are required to be Sync, so that they can be safely shared between threads. Use thread-safe state management utilities such as those found in std::sync to create stateful plugins.

The config parameter has been removed and replaced with an EngineInterface reference, which supports many more functions, including get_plugin_config() to get the config. For the other added functionality on EngineInterface, see that section.

LabeledError has been reworked, and now supports multiple labeled spans and some other attributes that miette provides. This helps to ensure that ShellError can generally be passed through a LabeledError and still appear the same to the user. A new ShellError::LabeledError variant is provided to contain it. More complex plugins might like to implement a miette::Diagnostic error, in which case converting to LabeledError can be done automatically via LabeledError::from_diagnostic().

A complete example of the new API organization from the plugin docs:

use nu_plugin::{serve_plugin, EvaluatedCall, JsonSerializer};
use nu_plugin::{EngineInterface, Plugin, PluginCommand, SimplePluginCommand};
use nu_protocol::{LabeledError, Signature, Type, Value};

struct LenPlugin;

impl Plugin for LenPlugin {
    fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {

struct Len;

impl SimplePluginCommand for Len {
    type Plugin = LenPlugin;

    fn name(&self) -> &str {

    fn usage(&self) -> &str {
        "calculates the length of its input"

    fn signature(&self) -> Signature {
            .input_output_type(Type::String, Type::Int)

    fn run(
        _plugin: &LenPlugin,
        _engine: &EngineInterface,
        call: &EvaluatedCall,
        input: &Value,
    ) -> Result<Value, LabeledError> {
        let span = input.span();
        match input {
            Value::String { val, .. } => Ok(Value::int(val.len() as i64, span)),
            _ => Err(
                LabeledError::new("Expected String input from pipeline").with_label(
                    format!("requires string input; got {}", input.get_type()),

fn main() {
    serve_plugin(&LenPlugin, JsonSerializer)

Plugin engine calls [toc]

The added EngineInterface parameter mentioned previously enables the following new functionality for plugins:

See the docsopen in new window for details and examples on what can be done with the EngineInterface.

Improved plugin custom values [toc]

Custom values returned by plugins previously had very limited functionality - they could really only be sent back to the same plugin in another command. This release expands the number of supported operations:

  • Cell paths (e.g. $custom_value.0 and $custom_value.field)
  • Operators (e.g. $custom_value + $other, $custom_value ++ "plain value")
  • Comparisons (for compatibility with sort)
  • Drop notification (useful for implementing handles)

Most of these improvements originated in #12088open in new window. Additionally, custom values are now also allowed to be used in examples for commands in plugins #12113open in new window.

For more information, see the plugins guide and CustomValue docsopen in new window.

Plugin test support crate [toc]

With plugins having much more functionality, we thought it would be nice if it were easy to write tests for your plugins, and even test your examples automatically. Now you can!

Add the nu-plugin-test-support crate to your dev-dependencies:

nu-plugin-test-support = "0.92.0"

Then test your examples:

fn test_examples() -> Result<(), nu_protocol::ShellError> {
    use nu_plugin_test_support::PluginTest;
    PluginTest::new("my_plugin", MyPlugin.into())?.test_command_examples(&MyCommand)

For more information, see the crate docsopen in new window and the contributor book.

Official plugin template [toc]

With this release, we are launching an official template for plugins, to help you get started. Use cargo-generateopen in new window:

> cargo generate --force --git
🤷   What will this plugin be named?: foo
Creating a new plugin named "foo"
Your plugin crate will be named "nu_plugin_foo".

Note that the MIT license is used by default, to reflect the majority of
Nushell projects. You can change this manually if you'd like to.

You must run cargo generate with --force, or it will rename your project to
something that is non-standard for Nushell plugins and this will fail.

If you see a message after this about renaming your project, please abort and
try again with --force.

🔧   Destination: /var/home/devyn/Projects/nushell/nu_plugin_foo ...
🔧   project-name: nu_plugin_foo ...
🔧   Generating template ...
🤷   What should your first command be called? (spaces are okay): foo
✔ 🤷   Do you intend to create more than one command / subcommand? · No
✔ 🤷   Would you like a simple command? Say no if you would like to use streaming. · Yes
🤷   What is your GitHub username? (Leave blank if you don't want to publish to GitHub) [default: ]:
🔧   Moving generated files into: `/var/home/devyn/Projects/nushell/nu_plugin_foo`...
🔧   Initializing a fresh Git repository
✨   Done! New project created /var/home/devyn/Projects/nushell/nu_plugin_foo
> cd nu_plugin_foo
> cargo build
> register target/debug/nu_plugin_foo
> foo Ferris
Hello, Ferris. How are you today?

Debugging support and proof-of-concept profiler [toc]

You may remember that we used to have a profile command which then got removed due to unsound implementation. Now, it's backopen in new window as debug profile! You can give it a closure and it will profile each pipeline element in it, stepping recursively into nested blocks/closures/command calls. Make sure to check its help message to understand its output and options.

Under the hood, the profiler uses a new general debugging API that is now hooked into the evaluation engine. The profiler is a proof-of-concept implementation using this API, but we imagine it could be used for other purposes, such as step debugging, code coverage, or even allowing to create custom debugger plugins.

A short user story as an example: The following screenshot shows the profiler output of sourcing kubouch's file: Basic profiling output You can see that most time is spent inside the load-env calls and in the if (is-windows) { .... We can increase the number of blocks to step into with the --max-depth flag which reveals more detail: Expanded profiling output You can notice that most of the expensive pipeline elements have one thing in common: The is-windows call. It is a custom command in kubouch's which internally calls (sys) which on kubouch's machine takes around 13 ms. Changing it to use $ and other smaller fixes decreased the startup time from 130ms to 50ms.

Support for binary data in explore [toc]

@zhiburtopen in new window has added the colored hexdump view for binary data to the explore command (#12184open in new window), making it much easier to page through the output. This should be handy for anyone who spends a lot of time looking at binary formats!

Colored hexdump in the explore command

Performance improvements [toc]

A significant effort in this release went into improving Nushell's performance and resource usage. As part of this, we grew our internal benchmark suite to measure the performance impact of future changes (#12025open in new window, #12293open in new window). In addition to performance improvements from the external command and plugin changes, there have been other notable efforts to make nushell faster! In #11654open in new window, @rtpgopen in new window reduced the work needed to clone stacks. This improvement will be most noticeable if you have very large global variables in the REPL, as these will no longer need to be copied in memory each time you hit enter. On top of this, @devynopen in new window made it cheaper to clone the engine state in #12229open in new window. This should make nushell more performant when evaluating closures and also should reduce memory usage (especially in the REPL). To further address memory usage @FilipAndersson245open in new window reduced the size of our Value primitive in #12252open in new window and with #12326open in new window @sholderbachopen in new window changed our internal Record to a more compact and efficient representation. During work we also identified and fixed inefficiencies like unnecessarily copying values when accessing columns in a table or record (#12325open in new window). Overall we hope to improve the experience with Nushell and welcome the community to contribute suggestions for benchmarks that reflect their workloads to avoid regressions and tune Nushell's engine.

Hall of fame [toc]

Bug fixes [toc]

Thanks to all the contributors below for helping us solve issues and bugs 🙏

@merelymyselfopen in new windowMake autocd return exit code 0#12337open in new window
@dead10ckopen in new windowinto sqlite: Fix insertion of null values#12328open in new window
@sholderbachopen in new windowFix return in filter closure eval#12292open in new window
@lavafrothopen in new windowfix: use environment variables to prevent command_not_found from recursing#11090open in new window
@YizhePKUopen in new windowFix: missing parse error when extra tokens are given to let bindings#12238open in new window
@dannou812open in new windowto json -r not removing whitespaces fix#11948open in new window
@JoaoFidalgo1403open in new windowFix usage of --tabs flag while converting to json#12251open in new window
@YizhePKUopen in new windowFix inaccurate sleep duration#12235open in new window
@IanManskeopen in new windowUse rest argument in export use to match use#12228open in new window
@saruboopen in new windowAdjust permissions using umask in mkdir#12207open in new window
@WindSoilderopen in new windowfix ls with empty string#12086open in new window
@rgwoodopen in new windowFix up ctrl+C handling in into sqlite#12130open in new window
@NowackiPatrykopen in new windowFix unexpected sqlite insert behaviour (attempt 2)#12128open in new window
@VlkrSopen in new windowFix build on OpenBSD#12111open in new window
@dj-sourbroughopen in new windowFix: lex now throws error on unbalanced closing parentheses (issue #11982)#12098open in new window
@NotTheDr01dsopen in new windowFix: Convert help example results to text#12078open in new window
@rgwoodopen in new windowRemove unused/incorrect input type from start#12107open in new window
@fdncredopen in new windowfix du --exclude globbing bug#12093open in new window

Enhancing the documentation [toc]

Thanks to all the contributors below for helping us making the documentation of Nushell commands better 🙏

@Jasha10open in new windowFix dead links in in new window
@AucaCoyanopen in new window♻️ rework some help strings#12306open in new window
@devynopen in new windowFix zip signature to mention closure input type#12216open in new window
@thomassimmeropen in new windowFix histogram error message#12197open in new window
@nils-degrootopen in new windowImprove error message for into sqlite with empty records#12149open in new window
@IanManskeopen in new windowFix broken doc link#12092open in new window
@wellweekopen in new windowremove repetitive word#12117open in new window
@sholderbachopen in new windowRemove outdated doccomment on EngineState#12158open in new window
@devynopen in new windowMisc doc fixes#12266open in new window
@fdncredopen in new windowcleanup coreutils tagging#12286open in new window

Our set of commands is evolving [toc]

New commands [toc]

This release adds four new commands and more flags to some existing commands.

plugin list [toc]

As part of the plugin persistence update, this command shows you not only all of the plugins you have installed and their commands, but also whether they are running and what their process ID is if they are:

> plugin list
 # │  name   │ is_running │   pid   │       filename        │ shell │           commands            │
 0 gstat true 1389890 .../nu_plugin_gstat ╭───┬───────╮
 0 gstat
 1 inc false .../nu_plugin_inc ╭───┬─────╮
 0 inc
 2 example false .../nu_plugin_example ╭───┬───────────────────────╮
 0 nu-example-1
 1 nu-example-2
 2 nu-example-3
 3 nu-example-config
 4 nu-example-disable-gc

You can join the output of this command on pid with ps in order to get information about the running plugins. For example, to see the memory usage of running plugins:

> plugin list | join (ps) pid | select name pid mem
 # │ name  │  pid   │   mem    │
 0 gstat 741572 10.2 MiB
 1 inc 741577  3.6 MiB

plugin stop [toc]

If you want to explicitly stop a plugin that's running in the background, you can use the plugin stop command. This works even if the plugin signals to Nushell that it wants to stay running.

> plugin stop inc
> plugin list | where is_running and name == inc
 empty list

Unlike kill, this does not send a signal to the process - it merely deregisters the plugin from Nushell's list of running plugins, which should eventually cause it to exit. If the plugin is unresponsive, you can kill its PID:

> plugin list | where is_running and name == inc | each { kill $ }

debug profile [toc]

Profile a closure, see debugging support and proof-of-concept profiler.

uname [toc]

Thanks to @dmatos2012open in new window, version 0.92.0 adds the uname command. Under the hood, it uses uutilsopen in new window to return a record containing system information.

query db --params [toc]

Thanks to @Doruminopen in new window, the --params flag was added to query db in #12249open in new window. This allows one to specify parameters in the SQL query instead of using string interpolation. This helps avoids potential SQL injection attacks.

detect columns --guess [toc]

A new flag, --guess, was added to detect columns in #12277open in new window. This uses a histogram approach to detect the boundaries between fixed-width columns, and may give better results for certain command outputs.

Changes to existing commands [toc]

echo [toc]

Breaking change

See a full overview of the breaking changes

echo will now never directly print values. It only returns its arguments, matching the behavior described in its help text. To print values to stdout or stderr, use the print command instead.

table [toc]

Breaking change

See a full overview of the breaking changes

With #12294open in new window, the table command will always pretty print binary values. It used to not if the next command was an external command.

into bits [toc]

Breaking change

See a full overview of the breaking changes

In #12313open in new window, into bits was changed so that it can no longer take date values as input. The current implementation of into binary (and until this release of into bits) for the date type is unstable and dependent on your system locale and timezone settings, as it creates a human-readable string instead of unique representation of the date-time point. We intend to deprecate and remove the into binary implementation for the date type in a future release.

nu-check [toc]

Breaking change

See a full overview of the breaking changes

nu-check received a significant refactor in #12317open in new window. It is now possible to check module directories, so the provided path no longer needs to end in .nu. As part of this, the --all flag was removed, since all modules can be parsed as a script.

mkdir [toc]

mkdir now uses uucore under the hood to determine the umask to apply to the directory being created (implemented in #12207open in new window).

ls [toc]

With #12086open in new window, ls "" no longer behaves like ls /. Instead, it will now report an error.

version [toc]

The version command has been changed to list plugin names in plugins. These reflect the name field of plugin list. The previous behavior was to list all plugin commands.

filter [toc]

The filter command now supports early returns in closures (fixed by #12292open in new window).

insert [toc]

With #12209open in new window, if a closure is used to insert new values, then the $in value for the closure will now have the same value as the first parameter of the closure (instead of often being null).

do [toc]

Closures passed to do may now take optional parameters and rest parameters. Additionally, type annotations on the parameters will be checked/enforced. See #12056open in new window for the relevant PR.

complete [toc]

complete now captures stderr output by default, and it is no longer necessary to use do in combination with complete.

ignore [toc]

In #12120open in new window, the ignore command has been patched to drop values it receives immediately rather than storing them first. This helps reduce the memory usage of large command output passed to ignore.

export use [toc]

In #12228open in new window, export use was changed to take a rest argument instead of an optional argument. This allows exporting items from nested modules at any arbitrary depth, and this now matches the existing behavior of use (without export).

sleep [toc]

Fixed an issue with sleep in #12235open in new window where the sleep duration would be rounded up to the nearest 100ms.

into sqlite [toc]

into sqlite has had a few bug fixes:

to json [toc]

With #11948open in new window, to json --raw now properly removes all whitespace. Additionally, the --tabs and --indent flags were fixed in #12251open in new window.

du [toc]

A bug with du was fixed in #12903open in new window where the --exclude flag would error complaining that it couldn't convert a glob to a string.

histogram [toc]

Improved the error message for when the column-name parameter was not provided (fixed by #12197open in new window).

into string [toc]

Custom values are now supported if their base value resolves to a string (#12231open in new window). We will likely extend this to other conversions in the future.

Deprecated commands [toc]

run-external flags [toc]

Breaking change

See a full overview of the breaking changes

All preexisting flags for run-external are now deprecated.

This is because run-external now interacts with pipe and file redirections just like if an external command had been run directly. I.e., run-external cmd should be the same as just ^cmd. The only difference is that run-external may have additional flags added in the future for other functionality.

  • To achieve the old --redirect-stdout behavior, simply use a pipe |.
  • Instead of --redirect-stderr, use the stderr pipe e>|, or use a regular pipe | for internal commands that use stderr directly (i.e., save --stderr and tee --stderr).
  • Instead of --redirect-combine, use the stdout and stderr pipe o+e>|.

As noted in external command output changes, external command output now has trailing new lines trimmed by default, so the --trim-end-newline flag was deprecated as well.

Removed commands [toc]

str escape-glob [toc]

str escape-glob was deprecated in the previous release (0.91.0), and it has now been removed.

Breaking changes [toc]

Full changelog [toc]