Overview#
Nushell is a command interpreter built around structured data. Every command emits a typed value (a scalar, record, list, or table) and every pipeline stage transforms a typed value. The interpreter parses each command, evaluates expressions, calls the built-in or plugin command, and forwards the result.
As a language, Nu is closer to a typed functional scripting
language than to a Unix shell. Variables are immutable by
default, types are real (int, float, string,
bool, date, duration, filesize, record,
list, table, binary, closure, cellpath),
and control flow uses expressions rather than exit-coded
commands. The trade is that bash’s “everything is a string”
escape hatches do not work; the operator has to think in types.
For the data-structure side (records, tables, lists), see Structures. For the wiring side (redirection, byte mode, host-shell boundary), see I/O and Pipelines.
Syntax#
A script lives in a .nu file and runs with nu.
$ cat hello.nu
$ # comments start with #
$ print "hello"
$ nu hello.nu
$ nu -c 'ls | first 5'
Identifiers#
Names are letters, digits, _, and -. Variables refer
back to their names with $name. Environment variables live
under $env ($env.PATH); shell metadata lives under
$nu ($nu.config-path).
let my-var = 1
let _hidden = "secret"
print $my-var
print $env.HOME
Variables#
let binds an immutable name. mut opts in to
mutability; the resulting variable still cannot leave its
defining scope. const is a compile-time constant.
let name = "operator"
mut count = 0
$count = $count + 1 # legal: mut
const VERSION = "1.0" # const
Reassigning a let is an error. The discipline is what makes
Nu pipelines safe to refactor.
Literals#
Real types, real literals.
42 # int
3.14 # float
"string" # string
'single-quoted string' # string, no interpolation
true / false # bool
2026-05-15 # date
30sec / 5min / 2hr / 7day # duration
1KiB / 1MB / 2GiB # filesize
[1 2 3] # list
{name: "operator", age: 36} # record
[[a b]; [1 2] [3 4]] # table (header + rows)
null # null
String interpolation#
Double-quoted strings interpolate with $(...) and bare
$name references inside a special $"..." form. Single
quotes never expand.
let name = "operator"
print $"hello ($name)" # hello operator
print 'hello $name' # hello $name (literal)
Operators#
Standard arithmetic, comparison, logical, and string operators
work as written. Regex match is =~; !~ for not-match.
in and not-in test list / record membership.
Operator |
Family |
Effect |
|---|---|---|
|
Arithmetic |
As written; |
|
Arithmetic |
Floor division |
|
Arithmetic |
Exponent |
|
Comparison |
As written |
|
Logical |
Boolean combinators |
|
Regex |
Match / not-match |
|
String |
Prefix / suffix test |
|
Membership |
Element-in-list, key-in-record |
Control#
Branching and looping use expressions with explicit types,
not exit codes. if returns a value; each is the
pipeline form of foreach; for is the imperative form.
If#
if is an expression. The branches must produce the same
type (or both produce nothing). else is optional but
typical.
let status = if (sys host | get hostname) =~ "prod" {
"production"
} else {
"non-prod"
}
print $status
flowchart LR
C{cond?} -->|true| T[then expr] --> X([result])
C -->|false| E[else expr] --> X
Each (pipeline)#
each applies a closure to every item in a list or table.
The default reach for “do this for every row”.
ls | each { |row| {name: $row.name, size: $row.size} }
1..5 | each { |n| $n * $n } # [1 4 9 16 25]
For (imperative)#
for loops the way bash does; useful when the body has side
effects rather than producing a transformed value.
for h in [web01 db01 cache01] {
print $"connecting to ($h)"
}
While#
mut i = 0
while $i < 5 {
print $i
$i = $i + 1
}
Match#
match is the pattern-matching primitive; far more expressive
than bash’s case. Patterns can match literals, types, list
structures, and record fields.
match $verb {
"get" => "read-only"
"put" | "post" => $"write: ($verb)"
_ => "unknown"
}
match $point {
{x: 0, y: 0} => "origin"
{x: 0, y: _} => "on y-axis"
{x: _, y: 0} => "on x-axis"
_ => "off-axis"
}
Custom Commands#
User-defined commands look like built-ins. Parameters take a type, multi-word names are allowed (quoted), and the result is a normal pipeline producer downstream verbs can filter or transform.
def "files larger than" [size: filesize] {
ls | where size > $size | sort-by size --reverse
}
files larger than 10MB
Type annotations validate the input at the boundary. The operator gets a real error if a caller passes a string where a filesize is wanted.
def greet [
name: string # required, typed
--greeting: string = "hello" # flag with default
--shout(-s) # boolean flag
] {
let msg = $"($greeting) ($name)"
if $shout { $msg | str upcase } else { $msg }
}
greet "operator" --shout
greet "alice" --greeting "hi"
I/O#
Nu reads and writes structured data natively. open parses by
file extension and returns the parsed value; save reverses
the trip when paired with a to <format> step. Streams
between commands carry typed values, not bytes; piping to
an external program switches into byte mode at the
boundary.
open data.json
open data.yaml
open data.toml
open data.csv
open data.xlsx
open data.parquet
open data.sqlite
ls | to csv | save inventory.csv
{a: 1, b: 2} | to json | save out.json
For the full surface (redirection, byte mode, the host-shell boundary), see I/O and Pipelines.
Errors#
Errors propagate as values. try catches them and returns
either the result of the body or the result of the catch
arm.
let result = try {
open missing.json
} catch {
{} # default
}
try {
http get https://target.example.com
} catch { |err|
print -e $"failed: ($err.msg)"
exit 1
}
error make raises a structured error.
def check [x: int] {
if $x < 0 {
error make {msg: "x must be non-negative", label: {text: "got"}}
}
$x
}
Modules#
A module is a .nu file with export def declarations.
use path/to/module.nu imports its commands. Modules can
also export environment changes (export-env) and constants.
# ~/.config/nushell/scripts/myutils.nu
export def hello [name: string] { $"hello ($name)" }
export def --env enter-project [path: path] {
cd $path
$env.PROJECT = ($path | path basename)
}
# In config
use ~/.config/nushell/scripts/myutils.nu *
hello "operator"
enter-project ~/src/coh
External Commands#
Anything not built into Nu (git, grep, ssh,
docker) runs as an external command. Nu pipes raw bytes to
externals and accepts raw bytes back. The trick is converting
between Nu’s structured world and the external world.
^git status # explicit external
git status # implicit (when no built-in shadows)
ls | to text | grep .go # render to bytes for grep
(^uname -r) | str trim # parse external output
The ^ prefix forces external resolution when a Nu command
shadows the name.
Runtime#
Nu is written in Rust and ships as a single binary. The
nu process parses each pipeline into an AST, evaluates
each stage in turn, and streams values through the next stage.
There is no fork on Nu-to-Nu transitions; each stage runs in
the same process. External commands fork and exec the way bash
does.
Practical implications.
Startup is fast because there is no compile step.
config.nuwork still adds latency; profile it.Streaming is real, large tables flow through pipelines without materialising in full unless a stage demands it (
sort, for example).Pre-1.0, breaking changes happen between minor versions; pin the version on shared scripts.
Library#
The standard library (std) ships built into Nu and
covers logging, asserts, dates, formatting, environment
manipulation, and operating-system probes.
use std
std log info "starting"
std assert ($x > 0) "x must be positive"
std datetime now
std help # the in-shell help
For the broader catalog, see Libraries.
Tasks#
Filter rows by predicate.
ls | where size > 1MB and modified > (date now) - 1hr
Iterate a file’s lines.
open input.txt | lines | each { |line| $line | str trim }
Read a file into a list of rows.
let users = (open users.json)
$users.0.name
Run a step with a timeout. (Drops to external timeout
since Nu has no native time bound on commands.)
timeout 30sec ^curl -fsSL https://target
Background work and wait for all jobs.
let pids = [
(bg-task &)
(bg-task &)
]
wait
(Nu does not have first-class job control; for parallel work
use par-each.)
[web01 db01 cache01] | par-each { |h| ssh $h uptime }
Diff two pipelines.
diff (open a.json | sort | to text) (open b.json | sort | to text)
References#
Nushell book (the upstream manual; types, pipelines, custom commands, scripting).
Nu cookbook (recipe catalog).
Nushell for the surrounding Nu section (Setup, init files, history, completion).
Patterns for error handling, immutability discipline, and bash-interop idioms.
Tools for the toolchain (
nu, plugins, debugging).The Terminal for the terminal-level view of scope and process nesting.
Comments#
Single-line comments start with
#. There are no block comments; chain#on adjacent lines.