Patterns#
A handful of patterns make Nu scripts safer and easier to live with. The big shifts from bash: errors are values you catch, variables are immutable by default, and the host-shell boundary (Nu ↔ external) is explicit, not silent.
Immutability First#
let is immutable. Reach for mut only when an
accumulator genuinely needs to change in place. Pipelines should
do almost all the work without state.
# idiomatic: transform without mutation
let total = (open users.json | get age | math sum)
# bash-style mutation, rare in idiomatic Nu
mut total = 0
for u in (open users.json) { $total = $total + $u.age }
The immutable form is also faster (no reallocation) and easier to refactor; mutable state shadows easily.
Error Handling#
Errors in Nu are structured values. try evaluates a
body; catch receives the error record. Compose
try / catch rather than reaching for set -e-style
strict mode.
let result = try {
open missing.json
} catch {
{} # default
}
try {
http get https://target.example.com
} catch { |err|
print -e $"failed: ($err.msg)"
exit 1
}
Raise a structured error from your own code with error
make.
def check [x: int] {
if $x < 0 {
error make {
msg: "x must be non-negative"
label: {text: "got negative", span: (metadata $x).span}
}
}
$x
}
Cleanup#
Nu does not have trap in the bash sense. The conventional
pattern is to wrap side-effecting work in a try and clean up
in the catch / in a finally-style block after.
let tmp = (^mktemp | str trim)
try {
# do work using $tmp
} catch { |err|
print -e $"error: ($err.msg)"
}
^rm -f $tmp
For longer-lived sessions, a wrapper function that always cleans up.
def with-temp [closure: closure] {
let tmp = (^mktemp | str trim)
try {
do $closure $tmp
} catch { |err|
^rm -f $tmp
error make {msg: $err.msg}
}
^rm -f $tmp
}
Argument Parsing#
Native. Type the parameters on the def line; the parser
handles the rest. Validation lives in attribute-free Nu code
via the type system; --help works automatically.
def deploy [
host: string # required positional
--env: string = "dev" # flag with default
--force(-f) # boolean flag
--port: int # optional flag
] {
if $env not-in [dev staging prod] {
error make {msg: $"bad env: ($env)"}
}
print $"deploying ($host) to ($env)"
}
Host-Shell Boundary#
External programs (^cmd) exchange bytes with Nu. The
operator usually wants to either keep work entirely in Nu (no
externals) or convert at the boundary.
Bytes → structure.
^cat data.json | from json
^uname -a | str trim
^ls /tmp | lines
Structure → bytes.
$users | to json | ^curl -X POST -d @- https://api.example.com
$rows | to csv | ^psql -c "\copy table from stdin csv"
Forgetting to convert is the most common Nu beginner bug. When
something silently produces [stream] or
[binary 4096 bytes] instead of a real value, a missing
from json / from csv / str trim is usually the cause.
Logging#
std/log ships with the standard library; use it instead of
inventing a logging format.
use std
std log info "starting"
std log warning "retry"
std log error $"fatal: ($err.msg)"
The output goes to stderr in a structured form, with timestamps and level prefixes; the operator can redirect or filter.
Quoting#
Single quotes are literal. Double quotes do not interpolate
unless prefixed with $. $"...($expr)..." is the
interpolation form.
let name = "operator"
"hello $name" # literal
$"hello ($name)" # hello operator
References#
Overview for the language-level error and immutability surface.
I/O and Pipelines for the host-shell boundary and the byte / value mode distinction.
Tools for
nu --version, the debugger, and profiling.