Nushell
9 min read

Nushell (nu) is a modern shell written in Rust that treats everything as structured data rather than raw text. Where traditional shells like bash pipe streams of bytes between commands, Nushell pipes typed values — lists, records, and tables — so the output of one command can be filtered, sorted, and reshaped with a consistent set of data commands regardless of the source (files, command output, JSON, an API, etc.).

It works cross-platform (Linux, macOS, Windows) and doubles as both an interactive shell and a scripting language with a real type system.

nu
ls | where size > 10mb | sort-by modified | first 5

This lists files, keeps only those over 10 MB, sorts by modification time, and takes the first five — all operating on a table, not text, so no awk/cut/sort parsing is needed.

The data-centric model

Most built-in commands return structured data that Nushell renders as a table:

nu
ls
# ╭───┬──────────────┬──────┬─────────┬──────────────╮
# │ # │ name │ type │ size │ modified │
# ├───┼──────────────┼──────┼─────────┼──────────────┤
# │ 0 │ README.md │ file │ 2.1 kB │ 2 hours ago │
# │ 1 │ src │ dir │ 128 B │ 1 day ago │
# ╰───┴──────────────┴──────┴─────────┴──────────────╯

Because it's data, you index and query it directly:

nu
ls | get name # extract one column → a list
ls | select name size # keep some columns → a narrower table
ls | get 0 # a single row → a record
ps | where cpu > 0 | sort-by cpu --reverse

Types of Data

Nushell has a rich type system. Primitive types:

TypeExample literals
int-65535, 0xff, 0o234, 0b1010
float9.99, -15.333, Infinity
string"hi", 'hi', `hi` , r#'raw'#
booltrue, false
datetime2000-01-01, date now
duration2min + 12sec, 3.14day
filesize64mb, 0.5kB
range0..4, 0..<5, 0.., ..4
binary0x[FE FF], 0b[1010]
nothingnull

Structured types build on those:

• list — ordered values: [0 1 'two' 3]
• record — key/value pairs: {name: "Nushell", lang: "Rust"}
• table — a list of records (rows + columns): [[x y]; [12 15] [8 9]]
• closure — anonymous function: {|e| $e + 1}
• cell-path — a path into structured data: $.name.0

Inspect any value's type with describe:

nu
42 | describe # => int
ls | describe # => table<name: string, type: string, ...>

Pipelines and $in

A pipeline threads a value through a series of commands. The special $in variable holds the pipeline input, letting you use it as an argument or mid-expression:

nu
"hello world" | str upcase
[1 2 3] | math sum
date now | $in + 1day # $in as part of an expression
ls | $in | length # $in collects the stream into a value

$in is most useful in custom commands and when the input isn't simply the first argument of the next command.


Access nested data with cell paths (dot syntax mixing string keys and integer row indices):

nu
let data = (open config.json)
$data.servers.0.host # key → row 0 → key
$data | get servers.0.host # equivalent via `get`
$data.servers?.host # optional ? → null instead of error if missing

Common shaping commands: get, select, where, first, last, skip, take, sort-by, group-by, uniq, flatten, update, insert, reject, rename, merge, transpose.

Loading and converting data

open parses known file types into structured data automatically (by extension), and the from/to families convert between formats explicitly — so Nushell is a capable data-wrangling tool over JSON, YAML, TOML, CSV, and more.

nu
open data.json # parsed into records/tables
open notes.txt # raw string (unknown type)
open --raw data.json # force raw text

'{"a":1,"b":2}' | from json # parse a string
ls | to csv # serialize a table to CSV
open data.csv | to json | save data.json
http get https://api.github.com/repos/nushell/nushell | get stargazers_count

Variables

let binds an immutable variable, mut a mutable one, and const a compile/parse-time constant. Type annotations are optional.

nu
let x = 10
let name: string = "World"
mut count = 0
$count += 1
const CONFIG_DIR = ("~/.config" | path expand)

Variables are immutable by default, which is idiomatic Nu — prefer pipelines over mutation where possible. Closures cannot mutate variables in the enclosing scope (use a loop with a block if you must).

Operators

Arithmetic + - * / // mod **; comparison == != < <= > >=; logic and or xor not (short-circuiting); membership in, not-in, has, not-has; strings/regex =~ (matches/contains), !~, starts-with, ends-with; list append ++; bitwise bit-or bit-and bit-xor bit-shl bit-shr.

nu
"foobar" =~ "o+" # regex match → true
"FOO" =~ "(?i)foo" # case-insensitive modifier
[1 2] ++ [3 4] # => [1 2 3 4]
"hello" in ["hello" "world"] # => true

The ... spread operator unpacks lists and records into list/record literals or command calls:

nu
let a = [1 2 3]
[0 ...$a 4] # => [0 1 2 3 4]
{...$base, extra: true} # merge into a record literal

Control flow

if/else and match are expressions (they return values); loops (for, while, loop) are statements.

nu
if $x > 0 { 'positive' } else if $x == 0 { 'zero' } else { 'negative' }

match $value {
1 => "one",
2 | 3 => "a few",
{name: 'bar', count: $n} if $n > 5 => $"many: ($n)", # pattern match + guard
_ => "other",
}

for x in [1 2 3] { print ($x * $x) }
mut i = 0; while $i < 5 { $i += 1 }
loop { if (date now | $in) > $deadline { break } }

Idiomatically, prefer the iteration commands over loops, since they're expressions that flow through pipelines:

nu
[1 2 3] | each {|x| $x + 1 } # map → [2 3 4]
ls | where type == dir | each {|d| $d.name }
[1 2 3 4] | reduce {|it, acc| $acc + $it } # fold → 10
[1 2 3] | par-each {|x| $x * 2 } # parallel map

Custom commands

Custom commands are first-class — they appear in help, participate in pipelines, and are type-checked. Define them with def. The value of the final expression is the implicit return.

nu
def greet [name: string] {
$"Hello, ($name)!"
}
greet "World" # => Hello, World!

Parameters support types, optionals (?), defaults, flags, and ...rest:

nu
def build [
target: string # required positional
--release (-r) # boolean flag, short form -r
--jobs (-j): int = 4 # flag with type and default
...features: string # rest parameter
] {
print $"building ($target), release=($release), jobs=($jobs)"
print $"features: ($features)"
}
build app -r -j 8 ssl tls

Declare pipeline input/output types in the signature, and document with comments (shown in help):

nu
# Double every number in the input list.
def double []: list<int> -> list<int> {
each {|x| $x * 2 }
}
[1 2 3] | double # => [2 4 6]

By default a command's environment changes don't leak to the caller. Use def --env to let it modify the caller's environment (e.g. cd), and def --wrapped to forward unknown flags to a wrapped external command.

Aliases

Aliases expand to another command (unlike custom commands, they substitute textually):

nu
alias ll = ls -l
alias g = git

Environment

The environment lives in the $env record. Set variables by assigning (the old let-env syntax is gone):

nu
$env.EDITOR = "hx"
$env.PATH ++= ["~/.local/bin"] # PATH is auto-converted to a list
$env.MY_VAR = "value"

with-env {DEBUG: "1"} { ./run.nu } # scoped env for one block

Environment changes are scoped to the current block/closure unless made in a def --env command. $env.config is the special record that holds all of Nushell's own settings.

Scripts

A .nu file is a script. Run it with nu script.nu; arguments are declared via a def main:

nu
#!/usr/bin/env nu
def main [name: string, --loud] {
let greeting = $"Hello, ($name)"
if $loud { print ($greeting | str upcase) } else { print $greeting }
}

nu
nu greet.nu World --loud

main is called automatically; subcommands can be exposed as def "main sub" [...].

Modules and overlays

Modules group commands, aliases, constants, and environment variables for reuse. Define inline with module, or treat any .nu file or directory as a module, then bring definitions into scope with use:

nu
module greetings {
export def hello [name] { $"Hello, ($name)!" }
export def bye [name] { $"Bye, ($name)!" }
}
use greetings hello
hello "World"

use std/log # standard-library module
use ./utils.nu * # import everything from a file module

Overlays are layered sets of definitions you can activate and deactivate as a unit — useful for environments like virtualenvs:

nu
overlay use ./myenv.nu # activate
overlay list # show active overlays
overlay hide myenv # deactivate, removing its definitions

Configuration

Nushell reads two main files from $nu.default-config-dir (~/.config/nushell on Linux):

• env.nu — historically for environment setup (now largely optional).
• config.nu — settings, custom commands, module imports, startup tasks.

Additional *.nu files in the autoload directories ($nu.user-autoload-dirs) are sourced at startup for a modular setup. Open them quickly with config nu and config env.

Settings live in the $env.config record. Assign individual keys rather than the whole record so you don't clobber defaults:

nu
$env.config.show_banner = false
$env.config.buffer_editor = "hx"
$env.config.history = {
file_format: sqlite
max_size: 1_000_000
sync_on_enter: true
}

View the documented defaults with config nu --doc | nu-highlight | less -R. The $nu constant exposes paths and runtime info ($nu.config-path, $nu.home-dir, $nu.os-info, $nu.is-interactive, …).

Hooks

Hooks run code at events such as before/after each prompt, on directory change, or on environment changes — configured under $env.config.hooks:

nu
$env.config.hooks.env_change.PWD = [
{|before, after| print $"entered ($after)" }
]

Plugins

Nushell is extensible via plugins (separate binaries that add commands). Register and manage them with plugin add / plugin use / plugin list. The search path is controlled by the NU_PLUGIN_DIRS constant.

nu
plugin add ~/.cargo/bin/nu_plugin_polars
plugin use polars

There's a broad plugin ecosystem (e.g. nu_plugin_polars for dataframes, clipboard, parquet, formats, and more) under the nushell organization and the community.

Background jobs

Long-running work can be backgrounded and managed with the job family: job spawn, job list, job kill.

nu
let id = (job spawn { sleep 30sec; "done" })
job list
job kill $id

Relationship to other shells

Nushell is not POSIX-compatible — scripts written for bash, zsh, or fish won't run unchanged, and it can't source their profiles. It runs external commands normally (prefix with ^ to bypass a shadowing built-in, e.g. ^ls), but its real strength is the structured-data pipeline. Its query operators lean on familiar concepts like regular expressions. Works on Linux, macOS, and Windows.

References

• Command reference — every built-in command
• The Nushell Book — official guide
• Cookbook — task-oriented recipes
• nu_scripts — community scripts and modules