Nushell is a shell built around typed, structured
data. Commands pass records, tables, and lists — not raw text — so most
sed/awk/cut munging disappears, and a different set of rules applies.
Rules below are distilled from the official Nushell Book and cited per section. Snippets are minimal and paraphrased; consult the linked pages for detail. Guidance targets Nushell 0.114 (the current release at time of writing) — see Version stability, which is itself a rule.
Nushell parses the entire script to an intermediate representation first,
then evaluates it. There is no eval, and nothing you compute at run time can
change what was already parsed.
(thinking in nu,
how nushell code gets run)
-
source,use, andoverlay useneed a parse-time-constant path. Their argument is resolved during parsing, before anylet/$envexists. Use aconst:const mods = ($nu.default-config-dir | path join "my-mods") use $"($mods)/utils.nu"
-
A runtime
if (... | path exists)guard cannot rescue a parse-timesource. The path is resolved at parse time regardless of theif, so a missing file still aborts startup withsourced_file_not_found. Generate the file earlier (inenv.nu, beforeconfig.nuis parsed) instead. This exact bug and its fix are demonstrated at runtime intests/nushell-startup-demo.sh. -
Do not build code as a string and
sourceit in the same file. It cannot exist at parse time. Only a small set of pure commands (e.g.path joinon$nu.*constants) can produce a parse-time constant.
-
Pipelines carry typed values. Reach for
get/select/where/updateon columns, not text tools. There is no implicit whitespace word-splitting. (coming from bash) -
Write files with
save, not>. In Nushell>is the greater-than operator, not redirection. (thinking in nu)"hello" | save output.txt # not: echo "hello" > output.txt
-
getunwraps values;selectkeeps a table. Choosing wrong changes the shape of everything downstream. (working with tables)ls | get name # a list of names ls | select name size # a two-column table
-
Edits are non-mutating.
update/insert/upsertreturn new data; useupsertwhen a column may or may not already exist. -
Convert types deliberately with
into/from; loaded data is often strings. (loading data)open people.txt | lines | split column "|" first last job | str trim
Prefer immutable bindings; they are what make Nushell's functional and parallel constructs safe. (variables)
letby default. Shadow in an inner scope rather than mutating.mutonly when you must reassign — and note amutvariable cannot be captured inside aneach/where/par-eachclosure.constfor anything needed at parse time (rule above).
-
Type the parameters and declare the input/output signature. Types are checked at parse time and document the command.
def increment [value: int]: nothing -> int { $value + 1 } def "str stats" []: string -> record { ... }
-
Return implicitly — the last expression is the result; there is no
returnkeyword to write. -
Model options as
--flags, optionals with defaults, variadics with...rest. -
A script's entry point is
def main [...]; subcommands aredef "main sub" [...].mainreceives typed CLI arguments. -
If a command must change the environment or
cd, declare itdef --env; otherwise its env mutations are discarded on return (env is block-scoped).
-
ifandmatchare expressions — assign or pipe their result instead of mutating a variable across branches.let sign = if $x > 0 { 'pos' } else if $x == 0 { 'zero' } else { 'neg' }
-
Prefer pipeline iteration (
each/where/reduce) overfor/while: it returns values and is dramatically faster (the docs cite ~19 ms vs ~64 s over 50k iterations). -
Parallelize independent per-row work with
par-each, thensort— order is not guaranteed. (parallelism) -
Sequence with
;; combine conditions withand/or(not Bash's&&). -
Reference pipeline input with
$in(it cannot cross a;— use a pipe). (pipelines)
- Put settings, commands, and aliases in
config.nu(the primary file);env.nuis legacy and loads first. Drop reusable snippets into an autoload dir ($nu.user-autoload-dirs) instead of growingconfig.nu. $envis block-scoped. Set with$env.FOO = ...; scope a variable to one command inline (FOO=bar some-cmd) rather than mutating global state.- Treat
$env.PATHas a list and mutate withprepend/append, not string concatenation. - Build and test paths with
pathbuiltins (path join,path exists) so separators stay correct cross-platform.
- Package reusable code as a module and
exportonly the public surface. - Use
export-env { ... }for env a module must apply on import — it runs at evaluation, not parse time. - Grow into a directory module with
mod.nu; name a default commandmain. - Use overlays for toggleable, virtual-env-style layers (
overlay use/overlay hide). The path must still be a parse-time constant.
(control flow, creating errors, navigating structured data)
-
Wrap fallible code in
try { } catch {|err| ... }and inspect the structured error record instead of letting it abort the script. -
Raise rich errors with
error makeusingmsg+label+span(from(metadata $x).span) so Nushell underlines the offending input. -
Access maybe-missing cells with an optional path (
get -o, or$x.col?) to getnullinstead of a hard error, thendefaultto fill it.$data | get temps?.1 | default 0
(running externals, stdout, stderr, exit codes)
-
Force an external with
^when a builtin shadows it, and quote its arguments yourself (no word-splitting).^git commit -m "message with spaces"
-
Capture results with
| completeto get{stdout, stderr, exit_code}and branch on the real status.let r = (^git status | complete) if $r.exit_code != 0 { print -e $r.stderr }
-
Tolerate an expected failure with
do -i { ^cmd }ortry; read$env.LAST_EXIT_CODEwhen you need the code.
Nushell is pre-1.0 and ships breaking changes on a roughly six-week cadence —
for example division began returning floats and mod was redesigned in
0.100.0, and find
became case-sensitive by default in
0.107.0.
-
Declare the Nushell version a script targets in a header comment, and optionally warn at runtime.
# targets Nushell 0.114 if (version).version != "0.114.1" { print -e "warning: untested Nu version" }
-
Do not compare version strings with
</>— they compare lexically, so"0.100.0" < "0.99.0"is true. Match exactly, or parse the components.
Explore commands from inside the shell — and note that help commands returns a
table you can filter like any other data.
(quick tour)
help <command>or<command> --help— documentation for one command.help commands— every builtin, as a table towhere/select.help --find <text>— search command docs.help commands | where is_const— the commands usable at parse time (the ones allowed in aconst, or in asource/usepath — see the mental model).
Static-check every *.nu with nu --ide-check N script.nu. Because parsing is a
separate stage, it reports parse and type errors as JSON without executing
the script — ideal for CI and pre-commit. This repo's task nushell
(tools/check-nushell.sh) fails on any "severity":"Error".
- Nushell Book — https://www.nushell.sh/book/
- Nushell release notes — https://www.nushell.sh/blog/
- The book is MIT-licensed (nushell/nushell.github.io); rules here are summarized, with links back to each source page.
See skills/shell for the agent-facing summary and
examples/nushell/ for parse-clean fragments.
Use -n (--no-config-file). Without it, nu -c loads the user's config.nu and
env.nu, so the command inherits their aliases, $env, and any parse-time source
— which makes an agent-run command non-deterministic and hostage to a config it did
not write. That is the same parse-time failure documented above, arriving through the
back door.
| Form | Use for |
|---|---|
nu -n -c '...' |
agent/CI one-liners — no config, no env, deterministic |
nu -n script.nu |
running a script file without user config |
nu --stdin -c '...' |
piping data in; read it via $in |
nu --no-newline -c '...' |
output destined for command substitution |
#!/usr/bin/env nu |
a standalone executable script |
nu -l / nu -i |
login / interactive shell — a human's shell, not a script's |
Startup cost is small when the config is lean (~35ms vs ~25ms on a tuned machine), so determinism, not speed, is the reason.
Check a script before shipping it:
nu --ide-check 0 script.nu # parse + type errors, no execution