nushell
/nushell
Write, review, and debug Nushell scripts and tests. Use when a task touches a `.nu` file or Nushell behaves unexpectedly.
- Author
- hakula139 ↗
- Repository
- plugins ↗
- Licence
- MIT
- Last repository change
- 12.11.2026
Assessment and security
- Files in this package
- 2
- Scripts
- 0
- HTML comments
- 0
- URLs in SKILL.md
- 6
No signals detected. Repository changed on 12.11.2026, without references to retired models or APIs.
MIT, open: use and modify while retaining author attribution.
What it does and when to use it
Write, review, and debug Nushell scripts and tests. Use when a task touches a `.nu` file or Nushell behaves unexpectedly.
Original author description
Write, review, and debug Nushell scripts and tests. Use when a task touches a `.nu` file or Nushell behaves unexpectedly.
How it works
- Scripts
- Traps
- Testing
- Linting
SKILL.md preview
---
name: nushell
description: >-
Write, review, and debug Nushell scripts and tests. Use when a task touches a `.nu` file or
Nushell behaves unexpectedly.
---
# Nushell
## Scripts
### Entry and help
- **Entry point**: `def main` with typed parameters.
- **Help**: limit the script header to its purpose and operating constraints, and document usage at each command and parameter so `--help` carries it.
### Modules and imports
- **Modules**: export only what another module or the module's own test calls, and keep other helpers private. Layer modules so imports point one way, with the entry importing domain modules and domain modules importing generic helpers.
- **Imports**: import a module by name when its commands share verbs with sibling modules, so calls read like `cache read` and `index read`. Otherwise list the imported commands, as in `use ./paths.nu [path-split path-label]`, so a reader sees where each name comes from. A test may `use ./<module>.nu *`.
### Layout and order
- **Order**: order a module's sections in the order the entry first calls them, and within a section put callees before callers.
- **Custom commands**: their switches stay on one line, as a builtin's do, since Nushell parses them itself.
### Quoting and strings
- **Quoting**: quote string values, including paths and path segments, so they read as data. Use double quotes, switching to single quotes when the text holds a literal backslash or double quote, as a regex does. Leave names, flags, subcommands, and a builtin's own words bare, as in `decode utf-8`. Leave a glob meant to expand bare, and build a home path from `$env.HOME? | default $nu.home-dir` because a quoted `~` stays literal. Keep the quotes any word needs to stay a literal string, as in `"0700"` and `"1min"`.
- **Multiline strings**: align the opening quote, content, and closing quote at the same column, then place `| str dedent` from `std-rfc/str` on the next line and any enclosing parenthesis on its own line.
### Paths
- **Path joins**: pass a literal relative path to `path join` as one `/`-separated string, as in `path join "libexec/gnubin"`, so it reads as the path it names, and give each segment that comes from a variable its own argument.
- **Script-relative paths**: locate files beside a script through a named constant at the top, as in `const SCRIPT_DIR = path self "."`, and join paths onto it. `path self` runs only at parse time and names the file it appears in, while `$env.FILE_PWD` and `$env.CURRENT_FILE` point at the running script even inside a module.
## Traps
These traps assume the Nushell Book's [Thinking in Nu](https://www.nushell.sh/book/thinking_in_nu.html) and [Coming from Bash](https://www.nushell.sh/book/coming_from_bash.html). Reproduce a trap on the pinned Nushell before adding it.
### Syntax
- **Bare words depend on context.** `where after != before` compares the `after` column with the string `"before"` (write `where after != $it.before`), and a comma splits a list item, so `[--sort=-pcpu,-pmem]` holds two items. Inside a list, an operator is a bare word too, so `["a" + $x]` holds three elements. Write `[("a" + $x)]` or `[$"a($x)"]`.
- **Outside parentheses or a list, a newline ends the command.** Arguments continue across lines only inside parentheses or a list, binary operators continue only inside parentheses, and a line starting with `|` continues the pipeline. A line starting with `and ($b)` calls a command named `and`, so wrap the whole expression in parentheses.
### Data and types
#### Input formats
- **`from json` accepts some plain text, so `try` cannot validate JSON.** `open` on an empty JSON, YAML, or NUON file returns null, while truncated JSON raises, so check the shape (`$j | describe | str starts-with "record"`) before reading a field.
- **YAML error messages hide the parser's detail.** Read `$e.rendered` in the catch block, since `$e.msg` says only `Error while parsing as yaml`.
- **`open --raw` yields a byte stream, which collects to `binary` on invalid UTF-8**, so a `: string` parameter or a `string ->` input raises a type error. To require UTF-8, pipe the stream itself into `into string` inside `try`, which raises `Non-UTF8 string` on invalid bytes and on a UTF-16 BOM. `into string` rejects every collected `binary`, valid or not.
#### Types and shapes
- **`get -o` raises on rows that are not records.** It tolerates a missing column but fails on a list of strings or a scalar, so wrap it in `try` when the shape is unverified.
- **Return annotations are checked only at parse time, against the type the parser infers for the body's last expression.** A `-> record` function can return a list from an untyped helper, and `get -o` on an empty list returns `[]`, which `default` keeps. A body that ends in a bare `error make` infers `error`, so a `die` helper declared `: nothing -> nothing` fails to parse and the script never runs, while one wrapped in `if`, `match`, or `do` passes. Declare `: nothing -> error` or omit the output type.
- **A `mut` or a defaulted closure parameter takes the type of its initial value.** Each later value is checked against that type, at parse time when its type is known and at runtime otherwise, so `mut current = null` rejects a record, `mut n = 0` rejects a float, and `generate {|row, acc = null| ... }` raises `Can't convert to nothing` once it passes `acc` a value. Annotate the union, as in `mut current: oneof<record, nothing> = null`, because a linter misses the runtime case.
#### Commands
- **A command that pulls a stream item and discards it drops that item's `exit` or `error make`.** `get <n>`, `last`, `skip`, `drop`, and `select` all do ([nushell/nushell#19463](https://github.com/nushell/nushell/issues/19463)), and [nushell/nushell#19467](https://github.com/nushell/nushell/pull/19467) lists the affected commands. The script continues, so only output the closure printed first, such as a `die` helper's message, still appears. `collect` before the command surfaces the error. Take a single match with `first`, which stops pulling once it has one item and returns null on an empty stream.
- **`math sum` errors on an empty list.** Use `| append 0 | math sum`.
- **`into datetime` reads a bare integer as nanoseconds.** A Unix timestamp in seconds needs `into datetime --format "%s"`.
- **`update` binds a named closure parameter to the row, not the cell.** Read the cell value from `$in`, as in `update body { $in | append $line }`.
### Strings and arguments
#### Strings
- **Dynamic regexes need single-quoted pieces.** Parentheses interpolate in both `$"..."` and `$'...'`, and double quotes consume backslashes, so concatenate them: `($label + ' (?<n>\d+)')`.
- **`url encode` escapes characters a URI keeps.** The default escapes every ASCII punctuation mark except `.`, `/`, and `:`, including the unreserved `-`, `_`, and `~`, and `--all` escapes those three as well. To match another encoder, pass only the characters it escapes through `url encode --all`.
- **`str length` counts UTF-8 bytes by default.** `"中文字" | str length` returns 9 on the pinned Nushell. Pass `--chars` or `--grapheme-clusters` when a limit is written in characters.
#### Arguments
- **Forwarding arguments needs `def --wrapped`.** A plain `def` parses flags in `...args` itself. `--wrapped` also passes `--help` through as an argument, so a command whose operands may start with `-` keeps a plain `def` and its callers pass `--`.
- **`nu <script> <args>` re-parses the arguments as source text.** Nushell rebuilds the `main` call from the arguments, quoting one only when it holds whitespace or a special character such as `;` or starts with `$`, so an untyped `main` parameter receives `8` as an int. Three shapes stay code on the pinned Nushell ([nushell/nushell#19484](https://github.com/nushell/nushell/issues/19484)): the name part of a `--` argument before any `=`, even after a positional (`'--x;print INJECTED'` prints `INJECTED` when `main` accepts `--x` or the argument follows `--`); a bare redirection token (`'o>' out.txt` sends `main`'s output to `out.txt`, truncating it even when `main` only prints); and an argument starting with `...$` (`'...$env.PATH'` spreads `PATH` into the arguments). Never pass an untrusted string as a script argument. Pass it through stdin, an environment variable, or a file, or check it against an allowlist pattern first.
- **Wrapper-provided scalar options supply defaults.** `makeWrapper --add-flag` puts them before caller arguments, and Nushell keeps the last repeated scalar value, so a later `--option <value>` selects that value when the command's contract permits it. Valued booleans use the same precedence, and a switch accepts an assignment such as `--force=false`. The fixed-value rule below applies only when a documented constraint makes a value immutable.
- **Use `makeWrapper --set` for a value a documented constraint makes immutable.** It exports an environment value for the script to read, so caller arguments cannot replace it through option precedence.
- **A flag on `def main` does not reach `def "main <sub>"`.** `script sub --flag` fails with an unknown flag, and `script --flag sub` passes `sub` to `main` as a positional, so declare a shared flag on each subcommand.
### External commands
- **`cp` reports a refused read or write as `nu: <error>` and still succeeds.** Use `^cp` when that failure must raise an error.
- **A nonzero external exit ends the script with that code and prints nothing.** Tolerate it with `try`, or by using `| complete` and checking `.exit_code`. Piping into `ignore` discards the exit status along with the output, so after `^cmd | ignore` the script continues and can exit 0.
- **Reading stdin requires `^cat`.** `$in` is empty under a shebang that lacks `--stdin`, and reopening `/dev/stdin` fails with ENXIO on a socket. Text capture strips one trailing newline, so use `^cat | into binary` for byte-identical round trips.
### Environment
- **`$nu.home-dir` ignores a `HOME` change inside the running Nushell.** It is fixed at startup, so code that runs after `$env.HOME = ...` or inside `with-env {HOME: ...}` in the same process reads `$env.HOME`. A child Nushell started with a new `HOME` sees it.
## Testing
Test a script with `std/assert` when its output is easy to get subtly wrong. Drive it the way its caller does, and skip tests that only restate the code or require the external tools the script exists to call. Run an entry script through `^$nu.current-exe`, which names the running Nushell binary, so the script runs on the same Nushell as its test.
### Files and assertions
- **Location**: a test sits beside the module or entry script it tests as `<name>.test.nu`.
- **Assertions**: compare the whole shape a test owns in one `assert equal`, so an unexpected field or row fails it.
### Inputs and fixtures
- **Structured inputs**: write JSON and JSONL test inputs as Nushell records and lists, serialized with `to json` for JSON or `to json --raw` per line for JSONL.
- **Fixtures**: when several cases differ only in document-like or byte-sensitive inputs, store them as golden files under the project's fixture directory, with one directory per case.
- A `case.nuon` file holds what the golden files cannot express. Write a line break as `\n`, except in a multi-line text string, which reads better as a multiline literal with continuation lines starting at column 0 because NUON preserves indentation as content. Format it like Nushell source, putting each list item on its own line.
- Keep small structured inputs in the test file.
- Copy each input into a `mktemp` directory before running the script so the script never writes to a fixture.
- **Timestamps**: fixtures use a fixed day in UTC, and a test that runs the script sets `TZ` for the child process, as in `with-env {TZ: "UTC"} { ^$nu.current-exe $script }`, so a local-time computation does not depend on the runner's zone, since the running Nushell reads its zone only at startup.
### The nu-test runner
devkit's [`nu-test`](https://github.com/hakula139/devkit/blob/v3/docs/nu-test.md) runs each command named `test <behavior>` in a `.test.nu` file as one test:
- **Names**: a test name may hold only letters, digits, spaces, and `,.:-`, since the runner passes it to `nu --commands` as source text, where a quote, `;`, or `#` ends or breaks the command.
- **One process per test**: the tests run in parallel, each in its own `nu` process, so a test that changes its environment or exits cannot affect the others.
- **Load failures**: a file that fails to parse, or whose test names cannot be read, reports a `(load)` entry with its output and counts as a failure. A file that prints to stdout or exits while loading cannot report its test names, so it fails too.
- **Stubs**: a `<name>.stub.nu` file beside a test file is installed as an executable `<name>` in the directory `NU_TEST_STUBS` names, with a shebang line the runner writes in place of any the stub has. Call a stub by `$env.NU_TEST_STUBS | path join <name>` or by prepending that directory to `PATH`.
- **Shared helpers**: a `<name>.testlib.nu` file beside the test files holds builders that several of them `use`. devkit's `nuTests` copies it with the tests, while its packaging leaves `.test.nu`, `.stub.nu`, and `.testlib.nu` files out.
- **Scratch space**: `TMPDIR` points to a fresh empty directory for each test, which the runner removes after the run, even if the file fails to load.
- **Exit codes**: the runner prints one `PASS` or `FAIL` line per test and a `passed, failed` summary, then exits nonzero if any test fails or no tests run.
## Linting
`nu --ide-check` exits 0 even when it reports an error, printing diagnostics as JSON, so inspect the severities. Without a max-errors argument, it exits 1 before reading the file. Because it catches delimiter errors and local `def` arity mismatches but treats a mistyped command as an external call, run the script once for real.
Don't format with `nufmt`. It reindents to four spaces by default, and even with `indent: 2` in its config it joins a pipeline split one stage per line, or a call split one option per line, back onto one line.
Files in this package (2)
- SKILL.md 14 KB
- agents/openai.yaml 1 KB
Versions
| Repository | Score | Licence |
|---|---|---|
| This page · hakula139/plugins | 100 | MIT |
| ryoppippi/dotfiles | 100 | MIT |
| DaRacci/nix-config | 100 | MIT |
| vinnie357/claude-skills | 100 | MIT |
Database snapshot: 13.11.2026 · package version 216f8e6f021c (SHA-256 digest of the entire package)
Downloads in the last 30 days: 0 · ranking
Report a problem with this skill: harmful content, incorrect descriptions or an author’s removal request.
Similar skills
- 100nushell-usage
Essential patterns, idioms, and gotchas for writing Nushell code. Use when writing Nushell scripts, functions, or working with Nushell's type system, pipelines, and data structures. Complements plugin development knowledge with practical usage patterns. - 100nushell-plugin-builder
Guide for creating Nushell plugins in Rust using nu_plugin and nu_protocol crates. Use when users want to build custom Nushell commands, extend Nushell with new functionality, create data transformations, or integrate external tools/APIs into Nushell. Covers project setup,… - 100gum-nushell
Use Gum from Nushell scripts to build interactive CLI/TUI flows. Trigger when writing, reviewing, or debugging Nu scripts that call `gum` commands such as choose, filter, confirm, input, write, file, table, spin, style, format, join, pager, log, or version-check, especially when… - 100koopa-nushell
Nushell (nu) pitfalls, parse-time constraints, and koopa activation architecture. Use when writing or debugging .nu files, editing lang/nushell/include/header.nu or any activate-*.nu, sourcing prompt tools (starship, zoxide), or porting shell logic from bash/zsh/fish to nushell. - 100nushell-data-processing
Structured data processing with nushell — native tables, multi-format parsing (JSON/YAML/TOML/CSV/XML), pipelines, group-by. Use when running multi-step cross-format transforms awkward in jq. - 100debug-rule
Debug a rule or approximation that behaves unexpectedly by tracing where taint is dropped. Use when its samples won't pass after repeated attempts, or it passes tests but is wrong on a real scan