name: nushell-style description: This skill should be used when writing, editing, reviewing, or debugging Nushell (.nu) files. Covers opinionated pipeline composition, command choices (where vs filter, match vs if/else, get --optional), formatting conventions (Topiary), type signatures, module structure, testing with nutest (unit tests, snapshot tests, @example attributes, coverage), NUON data format, toolkit.nu patterns, nu --ide-check debugging, the Nushell MCP server, and migration guide for updating scripts across Nushell versions (0.100–0.113: breaking changes, renamed commands, new idioms). Relevant when the user says "write nushell code," "review my .nu file," "nushell style," "nushell best practices," "format nushell," "nushell pipeline," "nutest," "NUON," "nu --ide-check," "nushell MCP," "update nushell script," "nushell breaking changes," or "nushell migration."
Nushell Code Style Guide
Contents
| File | Topic |
|---|---|
| This file | Quick reference tables, do/don't checklists |
| patterns.md | Pipeline composition, command examples, code structure |
| formatting.md | Topiary conventions, spacing, declarations |
| debugging.md | --ide-check for agents, diagnostic parsing |
| nuon.md | NUON format, data serialization, config files |
| testing.md | nutest framework, snapshots, coverage |
| toolkit.md | toolkit.nu, repo utilities, commit conventions |
| mcp.md | Nushell as MCP server (nu --mcp), tools, persistent state |
| migration.md | Breaking changes, renamed commands, new idioms (0.100 → 0.113) |
| enhancements.md | New features to improve existing scripts (0.100 → 0.113) |
Agent Tip: Syntax Checking
nu --ide-check 10 file.nu | nu --stdin -c 'lines | each { from json } | where type == "diagnostic"'
For line-number-aware diagnostics with source context, see the diagnose function in debugging.md.
Agent Tip: != and !~ in Bash
The Bash tool escapes ! → \!, breaking != and !~ in nu -c. Use a heredoc or temp file instead. See testing.md for workarounds.
Conciseness for Advanced Users
Write code that an experienced nushell user can quickly apprehend. Leverage implicit features:
| Verbose | Concise | Why |
|---|---|---|
update field {|row| $row.field | str upcase} |
update field { str upcase } |
Closure receives field value directly |
each {|x| $x | str trim} |
each { str trim } |
$in implicit, pipeline flows |
$list | each { str trim } |
$list | str trim |
Many commands accept list<string> directly (see below) |
where {|row| $row.status == "active"} |
where status == "active" |
where has field shorthand |
$data | each { $in | process } |
$data | each { process } |
$in passed automatically to first command |
Principle: If an advanced user knows how update, each, where work, they shouldn't need to parse redundant variable declarations.
Command Choices
| Task | Preferred | Avoid |
|---|---|---|
| Filtering | where |
filter, each {if} | compact |
| List filtering | where $it =~ ... |
where { $in =~ ... } |
| Parallel with order | par-each --keep-order |
par-each (when order matters) |
| Pattern dispatch | match expression |
Long if/else if chains |
| Record iteration | items {|k v| ...} |
Manual key extraction |
| Table grouping | group-by ... --to-table |
Manual grouping |
| Line joining | str join (char nl) |
to text (context dependent) |
| Syntax check (human) | nu -c 'open file.nu | nu-check' |
source file.nu |
| Syntax check (agent) | nu --ide-check 10 file.nu |
nu-check (unstructured) |
| Membership | in operator |
Multiple or conditions |
| Field extraction | get --optional |
each {$in.field?} | compact |
| Negation | $x !~ ... |
not ($x =~ ...) |
| List element ops | $list | str trim |
$list | each { str trim } |
Skip each When Commands Accept list<string>
Many commands accept both string and list<string> input — they operate on each element automatically. Wrapping them in each is redundant.
Heuristic: Check input_output types. If a command lists both string and list<string> as input, pipe the list directly.
# Check a command's accepted input types
help str trim | get input_output
# => [{input: string, output: string}, {input: list<string>, output: list<string>}, ...]
Common command families that accept list<string> directly: str (19 commands), path (9), split (4), into (4), ansi (3), url (2), fill.
# Preferred # Avoid
$list | str trim # $list | each { str trim }
$list | path expand # $list | each { path expand }
$list | ansi strip # $list | each { ansi strip }
$list | str replace 'a' 'b' # $list | each { str replace 'a' 'b' }
$list | split row ',' # $list | each { split row ',' }
$list | url encode # $list | each { url encode }
each IS needed when the command does not accept list input, or when the closure does more than a single command call.
Pipeline Principles
Leading |
Place | at start of continuation lines, aligned with let.
Omit $in |
When body starts with pipeline command (each, where, select), input flows automatically.
Empty { } Pass-Through
Use empty { } for the branch that should pass through unchanged:
| if $cond { transform } else { }— transform when true, pass through when false| if $cond { } else { transform }— pass through when true, transform when false
Stateful Transforms
Use scan for sequences with state: use std/iter scan
→ See patterns.md for detailed examples.
Script CLI Pattern
For toolkit-style scripts with subcommands (like nu toolkit.nu test):
# toolkit.nu
export def main [] { } # Entry point (required, even if empty)
export def 'main test' [--json] {
# nu toolkit.nu test
}
export def 'main build' [] {
# nu toolkit.nu build
}
Key points:
def main []— entry point when runningnu script.nudef 'main subcommand' []— definesnu script.nu subcommand- Must define
mainfor subcommands to be accessible - Use
export defif script is also used as a module
Script mode vs module mode
main is stripped in script mode but stays in module mode. export is irrelevant in script mode but required in module mode.
| How you run | Calls def "main test" |
export needed? |
|---|---|---|
nu toolkit.nu test |
✓ main stripped |
No |
use toolkit.nu; toolkit main test |
✓ main stays |
Yes |
use toolkit.nu *; main test |
✓ bare names | Yes |
⚠ Common agent mistake — using use (module mode) but calling with script-mode syntax:
# WRONG: script-mode syntax after module-mode import
use toolkit.nu
toolkit test # Error: extra positional argument
# CORRECT: include `main` in the command path
use toolkit.nu
toolkit main test # ✓
# OR: just use script mode
# nu toolkit.nu test # ✓
When in doubt, prefer script mode (nu script.nu subcommand) — it's simpler and avoids the main path issue.
→ See Nushell Scripts docs
Module Naming Rule
When a file is named after the command (e.g., greet.nu), the command must be named main, not the file name:
# File: greet.nu
# WRONG — "Can't export ... named same as the module"
export def greet [name: string] { $"Hello ($name)" }
# CORRECT — `main` becomes the module's default command
export def main [name: string] { $"Hello ($name)" }
After use greet.nu, call it as greet "world" — main is replaced by the module name. This applies to def, extern, and const.
Quick Reference
Do
- Omit
$in |when command body starts with pipeline command - Start continuation lines with
| - Use empty
else { }for pass-through - Use
matchfor type dispatch - Use
infor membership testing - Use
get --optionalfor field extraction - Use
scanfor stateful transforms - Use
wherefor filtering - Use
where $it =~ ...for list filtering - Combine consecutive
eachclosures when operations can be piped - Define data first, then filter
- Include type signatures:
]: input -> output { - Use
@exampleattributes (nutest) - Use
constfor static data - Keep custom commands focused
- Export ALL commands from implementation files (enables testing helpers)
- Control public API via
mod.nure-exports (not by removing exports) - Use
par-each --keep-orderfor parallel with deterministic output
Don't
- Start command bodies with
$in |when a pipeline command follows - Use spread operator
...with conditionals (use data-first +where) - Wrap external commands in unnecessary parentheses
- Over-extract helpers for one-time use
- Create wrapper commands that just call an existing command
- Use verbose names for local variables
- Break the pipeline flow unnecessarily
- Remove existing comments (preserve user's context)
- Remove
exportfrom helpers to "make them private" (use mod.nu instead) - Name a command the same as its file (use
maininstead — see Module Naming Rule)
Formatting Summary
- Run
topiary format <file>when available — it is the canonical formatter - Empty blocks:
{ }with space - Closures:
{ expr }with spaces - Flags:
--flag (-f)with space - Records: multi-line, no trailing comma
- Variables:
let x =(no$on left)
→ See formatting.md for full conventions.