
Nushell Pro
- 673 installs
- 12 repo stars
- Updated August 4, 2026
- hustcer/nushell-craft
nushell-pro is a Claude agent skill that replaces brittle bash scripts with robust, typed Nushell pipelines that coding agents can reliably edit and extend for shell automation workflows.
About
nushell-pro is an agent skill from hustcer/nushell-craft that teaches idiomatic Nushell scripting so developers and coding agents can replace fragile bash with typed, structured pipelines. The skill covers writing, reviewing, and extending .nu scripts with Nushell's data-first design, making automation easier for agents to parse and modify safely. Developers reach for nushell-pro when shell scripts break under agent edits, when data transformations need structured tables instead of string parsing, or when converting POSIX bash to maintainable Nushell modules. With 6 installs on Skills.sh in the nushell-craft bundle, nushell-pro is a focused niche skill for teams standardizing on Nushell for CLI automation alongside AI coding agents.
- Brings full Nushell language server and advanced completions into Claude Code, Cursor and Codex
- Converts complex shell one-liners into readable, testable Nushell scripts
- Enables safe filesystem, git, and API operations with structured data handling
- Reduces token usage by producing concise, declarative automation code
- Works with both local and remote Nushell environments
Nushell Pro by the numbers
- 673 all-time installs (skills.sh)
- Ranked #360 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hustcer/nushell-craft --skill nushell-proAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 673 |
|---|---|
| repo stars | ★ 12 |
| Last updated | August 4, 2026 |
| Repository | hustcer/nushell-craft ↗ |
How do you write agent-editable shell automation in Nushell?
Replace brittle bash scripts with robust, typed Nushell pipelines that agents can reliably edit and extend.
Who is it for?
Developers and agent workflows that automate data tasks with Nushell and need typed pipelines instead of fragile bash string parsing.
Skip if: Teams committed to POSIX bash-only environments or projects with no shell scripting or CLI automation requirements.
When should I use this skill?
A developer or agent is writing, reviewing, refactoring, or converting bash scripts to idiomatic typed Nushell pipelines.
What you get
Idiomatic .nu scripts, typed pipeline modules, and refactored Nushell replacements for brittle bash automation.
- .nu pipeline scripts
- refactored shell modules
By the numbers
- 6 Skills.sh installs in hustcer/nushell-craft
Files
Nushell Pro — Best Practices & Security Skill
Write idiomatic, performant, secure, and maintainable Nushell scripts. This skill enforces Nushell conventions, catches security issues, and helps avoid common pitfalls.
Core Principles
1. Think in pipelines — Data flows through pipelines; prefer functional transformations over imperative loops 2. Immutability first — Use let by default; only use mut when functional alternatives don't apply 3. Structured data — Nushell works with tables, records, and lists natively; leverage structured data over string parsing 4. Static parsing — All code is parsed before execution; source/use require parse-time constants 5. Implicit return — The last expression's value is the return value; no need for echo or return 6. Scoped environment — Environment changes are local to their block; use def --env when caller-side changes are needed 7. Type safety — Annotate parameter types and input/output signatures for better error detection and documentation 8. Parallel ready — Immutable code enables easy par-each parallelization
Critical: Pipeline Input vs Parameters
Pipeline input (`$in`) is NOT interchangeable with function parameters!
# WRONG — treats pipeline data as first parameter
def my-func [items: list, value: any] {
$items | append $value
}
# CORRECT — declares pipeline signature
def my-func [value: any]: list -> list {
$in | append $value
}
# Usage
[1 2 3] | my-func 4 # Works correctlyWhy this matters:
- Pipeline input can be lazily evaluated (streaming)
- Parameters are eagerly evaluated (loaded into memory)
- Different calling conventions entirely —
$list | func argvsfunc $list arg
Type signature forms
def func [x: int] { ... } # params only
def func []: string -> int { ... } # pipeline only
def func [x: int]: string -> int { ... } # both pipeline and params
def func []: [list -> list, string -> list] { ... } # multiple I/O typesNaming Conventions
| Entity | Convention | Example |
|---|---|---|
| Commands | kebab-case | fetch-user, build-all |
| Subcommands | kebab-case | "str my-cmd", date list-timezone |
| Flags | kebab-case | --all-caps, --output-dir |
| Variables/Params | snake_case | $user_id, $file_path |
| Environment vars | SCREAMING_SNAKE_CASE | $env.APP_VERSION |
| Constants | snake_case | const max_retries = 3 |
- Prefer full words over abbreviations unless widely known (
urlok,usrnot ok) - Flag variable access replaces dashes with underscores:
--all-caps->$all_caps
Formatting Rules
One-line format (default for short expressions)
[1 2 3] | each {|x| $x * 2 }
{name: 'Alice', age: 30}Multi-line format (scripts, >80 chars, nested structures)
[1 2 3 4] | each {|x|
$x * 2
}
[
{name: 'Alice', age: 30}
{name: 'Bob', age: 25}
]Spacing rules
- One space before and after
| - No space before
|params|in closures:{|x| ...}not{ |x| ...} - One space after
:in records:{x: 1}not{x:1} - Omit commas in lists:
[1 2 3]not[1, 2, 3] - No trailing spaces
- One space after
,when used (closure params, etc.)
Custom Commands Best Practices
Type annotations and I/O signatures
# Fully typed with I/O signature
def add-prefix [text: string, --prefix (-p): string = 'INFO']: nothing -> string {
$'($prefix): ($text)'
}
# Multiple I/O signatures
def to-list []: [
list -> list
string -> list
] {
# implementation
}Documentation with comments and attributes
# Fetch user data from the API
#
# Retrieves user information by ID and returns
# a structured record with all available fields.
@example 'Fetch user by ID' { fetch-user 42 }
@category 'network'
def fetch-user [
id: int # The user's unique identifier
--verbose (-v) # Show detailed request info
]: nothing -> record {
# implementation
}Parameter guidelines
- Maximum 2 positional parameters; use flags for the rest
- Provide both long and short flag names:
--output (-o): string - Use default values:
def greet [name: string = 'World'] - Use
?for optional positional params:def greet [name?: string] - Use rest params for variadic input:
def multi-greet [...names: string] - Use
def --wrappedto wrap external commands and forward unknown flags
Environment-modifying commands
def --env setup-project [] {
cd project-dir
$env.PROJECT_ROOT = (pwd)
}Data Manipulation Patterns
Working with records
{name: 'Alice', age: 30} # Create record
$rec1 | merge $rec2 # Merge (right-biased)
[$r1 $r2 $r3] | into record # Merge many records
$rec | update name {|r| $'Dr. ($r.name)' } # Update field
$rec | insert active true # Insert field
$rec | upsert count {|r| ($r.count? | default 0) + 1 } # Update or insert
$rec | reject password secret_key # Remove fields
$rec | select name age email # Keep only these fields
$rec | items {|k, v| $'($k): ($v)' } # Iterate key-value pairs
$rec | transpose key val # Convert to tableWorking with tables
$table | where age > 25 # Filter rows
$table | insert retired {|row| $row.age > 65 } # Add column
$table | rename -c {age: years} # Rename column
$table | group-by status --to-table # Group by field
$table | transpose name data # Transpose rows/columns
$table | join $other_table user_id # Inner join
$table | join --left $other user_id # Left joinWorking with lists
$list | enumerate | where {|e| $e.index > 5 } # Filter with index
$list | reduce --fold 0 {|it, acc| $acc + $it } # Accumulate
$list | window 3 # Sliding window
$list | chunks 100 # Process in batches
$list | flatten # Flatten nested listsNull safety
$record.field? # Returns null if missing (no error)
$record.field? | default 'N/A' # Provide fallback
if ($record.field? != null) { } # Check existence
$list | default -e $fallback # Default for empty collectionsPipeline & Functional Patterns
Prefer functional over imperative
# Bad — imperative with mutable variable
mut total = 0
for item in $items { $total += $item.price }
# Good — functional pipeline
$items | get price | math sum
# Bad — mutable counter
mut i = 0
for file in (ls) { print $'($i): ($file.name)'; $i += 1 }
# Good — enumerate
ls | enumerate | each {|it| $'($it.index): ($it.item.name)' }Iteration patterns
# each: transform each element
$list | each {|item| $item * 2 }
# each --flatten: stream outputs (turns list<list<T>> into list<T>)
ls *.txt | each --flatten {|f| open $f.name | lines } | find 'TODO'
# each --keep-empty: preserve null results
[1 2 3] | each --keep-empty {|e| if $e == 2 { 'found' } }
# par-each: parallel processing (I/O or CPU-bound)
$urls | par-each {|url| http get $url }
$urls | par-each --threads 4 {|url| http get $url }
# reduce: accumulate (first element is initial acc if no --fold)
[1 2 3 4] | reduce {|it, acc| $acc + $it }
# generate: create values from arbitrary sources without mut
generate {|state| { out: ($state * 2), next: ($state + 1) } } 1 | first 5Row conditions vs closures
# Row conditions — short-hand syntax, auto-expands $it
ls | where type == file # Simple and readable
$table | where size > 100 # Expands to: $it.size > 100
# Closures — full flexibility, can be stored and reused
let big_files = {|row| $row.size > 1mb }
ls | where $big_files
$list | where {$in > 10} # Use $in or parameterUse row conditions for simple field comparisons; use closures for complex logic or reusable conditions.
Pipeline input with $in
def double-all []: list<int> -> list<int> {
$in | each {|x| $x * 2 }
}
# Capture $in early when needed later (it's consumed on first use)
def process []: table -> table {
let input = $in
let count = $input | length
$input | first ($count // 2)
}Variable Best Practices
Prefer immutability
let config = (open config.toml)
let names = $config.users | get name
# Acceptable — mut when no functional alternative
mut retries = 0
loop {
if (try-connect) { break }
$retries += 1
if $retries >= 3 { error make {msg: 'Connection failed'} }
sleep 1sec
}Constants for parse-time values
const lib_path = 'src/lib.nu'
source $lib_path # Works: const is resolved at parse time
let lib_path = 'src/lib.nu'
source $lib_path # Error: let is runtime onlyClosures cannot capture mut
mut count = 0
ls | each {|f| $count += 1 } # Error! Closures can't capture mut
# Solutions:
ls | length # Use built-in commands
[1 2 3] | reduce {|x, acc| $acc + $x } # Use reduce
for f in (ls) { $count += 1 } # Use a loop if mutation truly neededString Conventions
Refer to String Formats Reference for the full priority and rules.
Quick summary (high to low priority):
1. Bare words in arrays: [foo bar baz] 2. Raw strings for regex: r#'(?:pattern)'# 3. Single quotes: 'simple string' 4. Single-quoted interpolation: $'Hello, ($name)!' 5. Double quotes only for escapes: "line1\nline2" 6. Double-quoted interpolation: $"tab:\t($value)\n" (only with escapes)
Modules & Scripts
Module structure
my-module/
├── mod.nu # Module entry point
├── utils.nu # Submodule
└── tests/
└── mod.nu # Test moduleExport rules
- Only
exportdefinitions are public; non-exported are private - Use
export def mainwhen command name matches module name - Use
export use submodule.nu *to re-export submodule commands - Use
export-envfor environment setup blocks
Script with main command and subcommands
#!/usr/bin/env nu
# Build the project
def "main build" [--release (-r)] {
print 'Building...'
}
# Run tests
def "main test" [--verbose (-v)] {
print 'Testing...'
}
def main [] {
print 'Usage: script.nu <build|test>'
}For stdin access in shebang scripts: #!/usr/bin/env -S nu --stdin
Error Handling
Custom errors with span info
def validate-age [age: int] {
if $age < 0 or $age > 150 {
error make {
msg: 'Invalid age value'
label: {
text: $'Age must be between 0 and 150, got ($age)'
span: (metadata $age).span
}
}
}
$age
}try/catch and graceful degradation
let result = try {
http get $url
} catch {|err|
print -e $'Request failed: ($err.msg)'
null
}
# Use complete for detailed external command error info
let result = (^some-external-cmd | complete)
if $result.exit_code != 0 {
print -e $'Error: ($result.stderr)'
}Suppress errors with do -i
do -i (ignore errors) runs a closure and suppresses any errors, returning null on failure. do -c (capture errors) catches errors and returns them as values.
# Ignore errors — returns null if the closure fails
do -i { rm non_existent_file }
# Use as a concise fallback
let val = (do -i { open config.toml | get setting } | default 'fallback')
# Capture errors as values (instead of aborting the pipeline)
let result = (do -c { ^some-cmd })When to use each approach:
do -i— Fire-and-forget, or when you only need a default on failuredo -c— Catch errors as values to abort downstream pipeline on failuretry/catch— When you need to inspect or log the errorcomplete— When you need exit code + stdout + stderr from external commands
Testing
Using std assert
use std/assert
for t in [[input expected]; [0 0] [1 1] [2 1] [5 5]] {
assert equal (fib $t.input) $t.expected
}Custom assertions
def "assert even" [number: int] {
assert ($number mod 2 == 0) --error-label {
text: $'($number) is not an even number'
span: (metadata $number).span
}
}Debugging Techniques
$value | describe # Inspect type
$data | each {|x| print $x; $x } # Print intermediate values (pass-through)
timeit { expensive-command } # Measure execution time
metadata $value # Inspect span and other metadataSecurity Best Practices
Refer to Security Reference for the full guide.
Nushell is safer than Bash by design (no eval, arguments passed as arrays not through shell), but security risks remain.
Never execute untrusted input as code
# DANGEROUS — arbitrary code execution
^nu -c $user_input
source $user_provided_file
# DANGEROUS — shell interprets the string
^sh -c $'echo ($user_input)'
^bash -c $user_inputSeparate commands from arguments (prevent injection)
# Bad — constructing command strings
let cmd = $'ls ($user_path)'
^sh -c $cmd
# Good — pass arguments directly (no shell interpretation)
^ls $user_path
run-external 'ls' $user_pathValidate and sanitize paths
# Bad — path traversal possible
def read-file [name: string] { open $name }
# Good — validate against traversal
def read-file [name: string, --base-dir: string = '.'] {
let full = ($base_dir | path join $name | path expand)
let base = ($base_dir | path expand)
if not ($full | str starts-with $base) {
error make {msg: $'Path traversal detected: ($name)'}
}
open $full
}Protect credentials
# Bad — credential visible to all child processes and in env
$env.API_KEY = 'secret-key-123'
^curl -H $'Authorization: Bearer ($env.API_KEY)' $url
# Good — scope credentials, use with-env
with-env {API_KEY: (open ~/.secrets/api_key | str trim)} {
^curl -H $'Authorization: Bearer ($env.API_KEY)' $url
}Safe file operations
# Bad — predictable temp file, race condition
let tmp = '/tmp/my-script-tmp'
'data' | save $tmp
# Good — use mktemp for unique temp files
let tmp = (^mktemp | str trim)
'data' | save $tmp
# ... use $tmp ...
rm $tmpHandle external command errors
let result = (^cargo build o+e>| complete)
if $result.exit_code != 0 {
error make {msg: $'Build failed: ($result.stderr)'}
}Safe rm operations
# Bad — glob from variable, could match unintended files
^rm $'($user_dir)/*'
# Good — validate then use trash or explicit paths
if ($user_dir | path type) == 'dir' {
rm -r $user_dir
}Script Review Checklist
Refer to Script Review Reference for the full checklist.
When reviewing a Nushell script, check these categories in order:
1. Security review (highest priority)
- [ ] No
nu -c/source/^sh -cwith untrusted input - [ ] No credential hardcoding or env leaking
- [ ] Paths from user input are validated (no traversal)
- [ ] External commands use argument separation (not string concatenation)
- [ ] Temp files use
mktemp, not predictable paths - [ ]
rmoperations are guarded and intentional
2. Correctness review
- [ ] Type annotations on all exported commands
- [ ] I/O pipeline signatures match actual behavior
- [ ] Error handling with
try/catchfor fallible operations - [ ] External commands checked with
completewhen error handling matters - [ ] Optional fields accessed with
?operator - [ ] No
foras final expression (useeachinstead) - [ ]
mutnot captured in closures
3. Style review
- [ ] Naming: kebab-case commands, snake_case variables
- [ ] String format priority followed
- [ ] Formatting: spacing, line length, multi-line rules
- [ ] Documentation comments on exported commands
- [ ]
^prefix on external commands - [ ] Functional style preferred over imperative
4. Performance review
- [ ]
par-eachfor I/O or CPU-bound parallel work - [ ]
each --flattenfor streaming when appropriate - [ ] Expensive computations cached in
letbindings - [ ] Large files streamed (lazy), not loaded entirely
Common Pitfalls
Refer to Anti-Patterns Reference for detailed explanations.
| Anti-Pattern | Fix |
|---|---|
echo $value | Just $value (implicit return) |
$"simple text" | 'simple text' (no interpolation needed) |
for as final expression | Use each (for doesn't return a value) |
mut for accumulation | Use reduce or math sum |
let path = ...; source $path | const path = ...; source $path |
"hello" > file.txt | `'hello' \ |
grep pattern | where $it =~ pattern or built-in find |
| Parsing string output | Use structured commands (ls, ps, http get) |
$env.FOO = bar inside def | Use def --env |
| `{ \ | x \ |
$record.missing (error) | $record.missing? (returns null) |
each on single record | Use items or transpose instead |
External cmd without ^ | Use ^grep to be explicit about externals |
Best Practices Summary
1. Use type signatures — Catch errors early, improve documentation 2. Prefer pipelines — More idiomatic, composable, and streamable 3. Document with comments — # above def for help integration 4. Export selectively — Don't pollute namespace 5. Use `default` — Handle null/missing gracefully 6. Validate inputs — Check types/ranges at function start 7. Return consistent types — Don't mix null and values unexpectedly 8. Use modules — Organize related functions 9. Prefix external commands with `^` — ^grep not grep; Nushell builtins take precedence (e.g., find is Nushell's, not Unix find) 10. Use external tools when faster — ^rg for large file search, ^jq for giant JSON
Workflow
When writing or reviewing Nushell code:
1. Read existing code to understand the context 2. Security audit — Check for injection, path traversal, credential leaks (see Security) 3. Check naming — kebab-case commands, snake_case variables 4. Check types — Add/verify type annotations and I/O signatures 5. Check strings — Follow the string format priority 6. Check patterns — Prefer functional pipelines over imperative loops 7. Check formatting — Spacing, line length, multi-line rules 8. Check documentation — Comments for exported commands, parameter descriptions 9. Check error handling — try/catch, complete for externals, validate inputs 10. Run validation if possible — nu -c 'source file.nu' or nu file.nu 11. Summarize changes made with security findings highlighted
References
- Security — Security hardening, threat model, safe patterns
- Script Review — Comprehensive review checklist
- String Formats — String type priority and conversion rules
- Anti-Patterns — Common mistakes with detailed fixes
- Data & Type System — Type hierarchy, collections, conversions, type guards
- Advanced Patterns — Performance, streaming, closures, memory efficiency
- Modules & Scripts — Module system, testing, attributes
- Bash to Nushell — Conversion guide from Bash/POSIX
Getting Help
- Use
nu -c 'help <command>'to check command signatures and examples - Use Nushell MCP tools for evaluating and testing Nushell code
- Consult the Nushell Book for in-depth documentation
nushell-pro
Nushell best practices, security hardening, and code review skill for Agents.
Write idiomatic, performant, secure, and maintainable Nushell scripts — with built-in code review, anti-pattern detection, and Bash-to-Nushell conversion.
Features
- Best Practices — Naming conventions, type annotations, I/O signatures, functional pipeline style, string format priority, and formatting rules
- Security Hardening — Injection prevention, path traversal protection, credential scoping, safe file/temp operations, environment sanitization
- Code Review — Comprehensive checklist covering security, correctness, style, performance, and robustness
- Anti-Pattern Detection — 23 common mistakes with idiomatic fixes
- Type System — Type hierarchy, complex types, type guards, null safety patterns
- Bash Conversion — Side-by-side Bash-to-Nushell translation guide
- Performance — Parallel processing with
par-each, streaming patterns, memory-efficient techniques
Install
# Install by npx skills
npx skills add hustcer/nushell-pro
# OR Install for Claude by claude cli
claude skill add --name nushell-pro hustcer/nushell-proOr clone manually into your skills directory:
git clone https://github.com/hustcer/nushell-pro.git ~/.claude/skills/nushell-proStructure
nushell-pro/
├── SKILL.md # Main skill (core rules, always loaded)
└── references/
├── security.md # Threat model, safe patterns, Windows risks
├── script-review.md # Full review checklist (5 categories)
├── anti-patterns.md # 23 anti-patterns with fixes
├── data-and-types.md # Type system, collections, conversions
├── advanced-patterns.md # Streaming, closures, parallel, debugging
├── modules-and-scripts.md # Modules, exports, testing, attributes
├── string-formats.md # String type priority and rules
└── bash-to-nushell.md # Bash/POSIX conversion guideSKILL.md is always loaded into context. Reference files are loaded on demand when the task requires deeper knowledge on a specific topic.
What It Covers
Core Principles
1. Think in pipelines — data flows through functional transformations 2. Immutability first — let by default, mut only when necessary 3. Structured data — tables, records, and lists over string parsing 4. Static parsing — source/use require parse-time constants 5. Implicit return — last expression is the return value 6. Scoped environment — def --env when caller-side changes are needed 7. Type safety — annotate parameters and I/O signatures 8. Parallel ready — immutable code enables easy par-each
Security Model
Nushell is safer than Bash by design (no eval, arguments passed as arrays), but risks remain:
| Risk Level | Threats |
|---|---|
| Critical | Code injection via nu -c, ^sh -c, plugin injection |
| High | Path traversal, credential leaks, PATH hijacking, glob injection |
| Medium | TOCTOU races, temp file races, unhandled errors, config tampering |
Script Review
The skill includes a 5-category review checklist:
1. Security (critical) — injection, paths, credentials, destructive ops 2. Correctness — types, errors, null safety, logic 3. Style — naming, strings, formatting, documentation 4. Performance — parallelism, streaming, caching 5. Robustness — input validation, file safety, process management
License
MIT
Advanced Nushell Patterns Reference
Performance Optimization
Lazy vs eager evaluation
# Lazy (streaming) — memory efficient for large data, single-pass
open large.csv | where status == 'active' | first 10
# Eager (load all) — faster for small data with multiple operations
let data = (open large.csv | where status == 'active')
$data | first 10
$data | last 10
$data | lengthUse lazy when: Large files/streams, single-pass operations, memory constrained Use eager when: Small datasets (<10k rows), multiple operations on same data, random access needed
Avoid repeated computation
# Bad — computes expensive-func 3 times
if (expensive-func) > 10 {
print (expensive-func)
save-to-file (expensive-func)
}
# Good — compute once
let result = expensive-func
if $result > 10 {
print $result
save-to-file $result
}Parallel processing
# Sequential
$urls | each {|url| http get $url }
# Parallel — faster for I/O operations
$urls | par-each {|url| http get $url }
# With thread pool size
$urls | par-each --threads 4 {|url| http get $url }Best for: I/O operations, CPU-intensive transforms, independent operations Avoid for: Small lists (overhead > benefit), side effects, order-dependent processing
Stream flattening with each --flatten
# Without --flatten: waits for each stream to complete, returns list<list<string>>
ls *.txt | each {|f| open $f.name | lines }
# With --flatten: streams items as they arrive, returns list<string>
ls *.txt | each --flatten {|f| open $f.name | lines }
# Practical: search across files without waiting for all to load
ls **/*.nu | each --flatten {|f|
open $f.name | lines | find 'export def'
} | str join (char nl)Memory-Efficient Patterns
Processing large files
# Bad — loads entire file into memory then filters
open large.log | lines | where {$in =~ 'ERROR'}
# Good — streams line by line
open large.log | lines | each --flatten {|line|
if ($line =~ 'ERROR') { $line }
}Batched processing
# Process in chunks of 1000
open large.csv | chunks 1000 | each {|batch|
$batch | process-batch
} | flattenAdvanced Closure Patterns
Closure composition
let double = {|x| $x * 2 }
let add_ten = {|x| $x + 10 }
# Compose manually
[1 2 3] | each {|x| do $add_ten (do $double $x) }
# Result: [12, 14, 16]
# Or build a composed closure
let transform = {|x| do $double $x | do $add_ten $in }
[1 2 3] | each $transformClosure currying pattern
def make-multiplier [factor: int] {
{|x| $x * $factor }
}
let triple = (make-multiplier 3)
let quadruple = (make-multiplier 4)
[1 2 3] | each $triple # [3, 6, 9]
[1 2 3] | each $quadruple # [4, 8, 12]Closures capture environment (immutable only)
let multiplier = 10
let compute = {|x| ($x * 2) + $multiplier }
do $compute 5 # 20
# Mutable variables CANNOT be captured in closures
mut sum = 0
[1 2 3] | each {|x| $sum += $x } # Error!
# Use reduce instead
let sum = [1 2 3] | reduce {|x, acc| $acc + $x }Stream Patterns
Generate infinite sequences
# Fibonacci using generate
generate {|state|
let a = $state.0
let b = $state.1
{out: $a, next: [$b, ($a + $b)]}
} [0, 1] | first 10Stream control
$stream | skip while {|x| $x < 100 } # Skip until condition false
$stream | take while {|x| $x < 1000 } # Take until condition false
[1 2 3 4 5] | window 3 # Sliding window: [[1,2,3], [2,3,4], [3,4,5]]
$data | chunks 100 # Fixed-size batchesAdvanced Error Handling
Graceful degradation
def robust-fetch [url: string] {
try {
http get $url
} catch {
try {
^curl -s $url | from json
} catch {
{error: 'All fetch methods failed'}
}
}
}External command error handling with complete
let result = (^cargo build o+e>| complete)
if $result.exit_code != 0 {
print -e $'Build failed:\n($result.stderr)'
} else {
print 'Build succeeded'
}Suppress errors with do -i / do -c
do -i (ignore errors) runs a closure and suppresses errors, returning null on failure. do -c (capture errors) catches errors and returns them as values.
# Fire-and-forget — silently ignore failure
do -i { rm $old_file }
# Concise default value pattern
let config = (do -i { open settings.toml } | default {})
# Capture errors as values (useful to abort downstream pipeline)
let result = (do -c { ^failing-cmd })
# Compare error handling approaches:
# do -i — suppress error, return null (simplest)
# do -c — catch error as value, abort downstream pipeline on failure
# try/catch — inspect/log/recover from errors
# complete — full exit_code + stdout + stderr for externalsAdvanced Glob Patterns
# Multiple extensions
glob **/*.{rs,toml,md}
# Exclusions
glob **/*.rs --exclude [**/target/** **/tests/**]
glob **/tsconfig.json --exclude [**/node_modules/**]
# Character classes
glob '[Cc]*' # Files starting with C or c
glob '[!0-9]*' # Files NOT starting with digit
glob 'src/[a-m]*.rs' # Files starting with a-m
# Depth limit
glob **/*.rs --depth 2 # Max 2 directories deep
# Directory only
glob '[A-Z]*' --no-file --no-symlink # Only directories starting with uppercase
# Follow symlinks
glob '**/*.txt' --follow-symlinks
# Case-insensitive (wax syntax)
glob '(?i)readme*'Custom Data Types with Structured Output
def make-report [title: string, data: table]: nothing -> record {
{
title: $title
generated: (date now)
row_count: ($data | length)
columns: ($data | columns)
data: $data
}
}Row Conditions vs Closures (Deep Dive)
Row conditions — short-hand syntax
# Left side auto-expands to $it.field
$table | where size > 100 # $it.size > 100
$table | where name =~ 'test' # $it.name =~ 'test'
ls | where type == file # Simple and readable
# Limitation: subexpressions need explicit $it
ls | where ($it.name | str downcase) =~ readmeClosures — full flexibility
$table | where {|row| $row.size > 100 }
$table | where {$in.size > 100 }
# Can be stored and reused
let big_files = {|row| $row.size > 1mb }
ls | where $big_files
# Works anywhere
$list | each {|x| $x * 2 }Row conditions: Simple field comparisons (cleaner syntax), cannot be stored in variables Closures: Complex logic, reusable conditions, nested operations
Iteration Pitfalls
each on single records
# Bad — runs only once, not iterating fields!
let rec = {a: 1, b: 2}
$rec | each {|field| print $field } # Only runs once
# Good — use items, values, or transpose
$rec | items {|key, val| print $'($key): ($val)' }
$rec | transpose key val | each {|row| ... }Pipe vs call ambiguity
# These are different!
$list | my-func arg1 arg2 # $list piped as input, arg1 & arg2 as params
my-func $list arg1 arg2 # All three as positional params (if signature allows)Debugging Techniques
# Inspect type
$value | describe
# Print intermediate values without breaking pipeline
$data | each {|x| print $x; $x }
# Measure execution time
timeit { expensive-command }
# Inspect metadata (span info for error reporting)
metadata $value
# View full command signature
help my-command
scope commands | where name == 'my-command'Nushell Anti-Patterns Reference
Common mistakes and their idiomatic fixes when writing Nushell scripts.
1. Using echo Instead of Implicit Return
Nushell implicitly returns the last expression's value. echo is almost never needed.
# Bad
def greet [name: string]: nothing -> string {
echo $'Hello, ($name)!'
}
# Good
def greet [name: string]: nothing -> string {
$'Hello, ($name)!'
}Use print when you want to display a message as a side effect (not as return value):
def process [] {
print 'Processing...' # Side effect: displayed to user
do-work # Return value: result of do-work
}2. Using for as Final Expression
for is a statement that returns null. Use each for transformations.
# Bad — returns nothing
def squares []: nothing -> list<int> {
for x in [1 2 3 4] { $x ** 2 }
}
# Good — returns the list
def squares []: nothing -> list<int> {
[1 2 3 4] | each {|x| $x ** 2 }
}3. Mutable Variables for Accumulation
# Bad — imperative accumulation
mut total = 0
for item in $items { $total += $item.price }
# Good — math sum
$items | get price | math sum
# Bad — building a list with mutation
mut result = []
for f in (ls) {
if ($f.size > 1mb) { $result = ($result | append $f.name) }
}
# Good — filter pipeline
ls | where size > 1mb | get name4. Dynamic source/use Paths
Nushell parses all code before evaluation. source/use require parse-time constant paths.
# Bad — let is evaluated at runtime
let my_path = '~/scripts'
source $'($my_path)/utils.nu' # Error!
# Good — use const for parse-time resolution
const my_path = '~/scripts'
source $'($my_path)/utils.nu'5. Bash-Style Redirection
# Bad — > is the comparison operator in Nushell
'hello' > file.txt # This is a boolean comparison, not redirection!
# Good — use save command
'hello' | save file.txt
'hello' | save --append file.txt6. String Parsing External Commands
# Bad — parsing ls output as strings
^ls -la | lines | each {|l| $l | split column ' ' }
# Good — use Nushell's structured ls
ls -la
# Bad — parsing JSON from curl
^curl -s https://api.example.com | from json
# Good — use http get (returns structured data directly)
http get https://api.example.com7. Ignoring Type Annotations
# Bad — untyped, hard to catch errors
def process [data] { $data | get name }
# Good — typed, catches misuse at parse time
def process [data: record<name: string, age: int>]: nothing -> string {
$data.name
}
# Good — I/O signature
def double []: int -> int { $in * 2 }8. Space Before Closure Parameters
# Bad — space before |params|
ls | each { |f| $f.name }
# Good — no space before |params|
ls | each {|f| $f.name }9. Environment Changes in Regular def
# Bad — cd change is lost after command returns
def go-project [] { cd ~/projects/my-app }
go-project
pwd # Still in original directory!
# Good — use def --env to propagate environment changes
def --env go-project [] { cd ~/projects/my-app }
go-project
pwd # Now in ~/projects/my-app10. Unnecessary String Interpolation
# Bad — interpolation with no variables
let msg = $"hello world"
# Good — simple string
let msg = 'hello world'
# Bad — double-quoted interpolation without escapes
let greeting = $"Hello, ($name)!"
# Good — single-quoted interpolation (no escapes needed)
let greeting = $'Hello, ($name)!'11. Using each When par-each Works
# Suboptimal — sequential file processing
ls **/*.json | each {|f| open $f.name | get version }
# Better — parallel processing for I/O bound work
ls **/*.json | par-each {|f| open $f.name | get version }Use each only when: order must be preserved, side effects must be sequential, or list is very small.
12. Missing Command Documentation
# Bad — no documentation
def deploy [env, --force] { ... }
# Good — documented command
# Deploy the application to the specified environment
#
# Handles building, testing, and deployment in one step.
@example 'Deploy to staging' { deploy staging }
def deploy [
env: string # Target environment (staging, production)
--force (-f) # Skip confirmation prompts
] { ... }13. Not Using default for Optional Values
# Bad — verbose null check
let name = if $input == null { 'anonymous' } else { $input }
# Good — use default command
let name = $input | default 'anonymous'14. Manual JSON/YAML/TOML Parsing
# Bad — manual string manipulation
let version = (open Cargo.toml | lines | where $it =~ '^version' | first | split column '=' | get column2.0 | str trim)
# Good — native structured data support
let version = (open Cargo.toml | get package.version)15. Not Using match for Multi-Branch Logic
# Bad — chain of if/else
if $status == 'ok' { handle-ok }
else if $status == 'error' { handle-error }
else if $status == 'pending' { handle-pending }
else { handle-unknown }
# Good — pattern matching
match $status {
ok => { handle-ok }
error => { handle-error }
pending => { handle-pending }
_ => { handle-unknown }
}16. Incorrect Shebang for stdin Scripts
# Bad — script won't receive stdin
#!/usr/bin/env nu
# Good — add --stdin flag
#!/usr/bin/env -S nu --stdin
def main [] { $in | process }17. Forgetting export in Modules
# Bad — command is private, can't be imported
# my-module.nu
def helper [] { 'hello' }
# Good — export makes it public
export def helper [] { 'hello' }
# Also good — keep internal helpers private intentionally
def internal-helper [] { 'private' }
export def public-cmd [] { internal-helper }18. Confusing Pipeline Input with Parameters
# Bad — treats pipeline data as positional parameter
def my-func [items: list, value: any] {
$items | append $value
}
# Good — declares pipeline input signature
def my-func [value: any]: list -> list {
$in | append $value
}
# Usage: [1 2 3] | my-func 4Why: Pipeline input is lazily evaluated (streaming); parameters are eagerly loaded. Different calling conventions entirely.
19. Using each on Single Records
# Bad — runs only once, not iterating fields!
let rec = {a: 1, b: 2}
$rec | each {|field| print $field }
# Good — iterate key-value pairs
$rec | items {|key, val| print $'($key): ($val)' }
$rec | transpose key val | each {|row| ... }20. Accessing Missing Fields Without ?
# Bad — error if field doesn't exist
$record.missing_field # Error!
# Good — use ? for optional access
$record.missing_field? # Returns null
$record.missing_field? | default 0 # Provide fallback21. Not Prefixing External Commands with ^
# Ambiguous — could be Nushell builtin or external
find pattern # This is Nushell's find, NOT Unix find!
sort # This is Nushell's sort, NOT Unix sort!
# Clear — explicitly calls external
^find . -name '*.rs' # Unix find
^sort file.txt # Unix sort
^grep pattern file # External grepRule: Nushell builtins always take precedence. Use ^ to unambiguously call external commands.
22. Ignoring complete for External Command Errors
# Bad — no error handling for external commands
let output = (^cargo build)
# Good — use complete for full error info
let result = (^cargo build o+e>| complete)
if $result.exit_code != 0 {
print -e $'Build failed:\n($result.stderr)'
}23. Empty Collection Checks
# Bad — comparing length
if ($list | length) == 0 { ... }
# Good — use is-empty / is-not-empty
if ($list | is-empty) { ... }
if ($list | is-not-empty) { ... }Bash to Nushell Conversion Reference
Quick reference for converting common Bash patterns to idiomatic Nushell.
Redirections & Pipes
| Bash | Nushell | Notes |
|---|---|---|
echo "text" > file | `'text' \ | save file` |
echo "text" >> file | `'text' \ | save --append file` |
cmd 2>/dev/null | `cmd e>\ | ignore` |
cmd > /dev/null 2>&1 | `cmd o+e>\ | ignore` |
cmd 2>&1 | `cmd o+e>\ | ...` |
| `cmd1 \ | tee log.txt \ | cmd2` |
| `cmd \ | head -5` | `cmd \ |
| `cmd \ | tail -3` | `cmd \ |
Variables
| Bash | Nushell | Notes |
|---|---|---|
FOO="bar" | let foo = 'bar' | Immutable by default |
FOO="bar" | mut foo = 'bar' | When mutation needed |
readonly FOO="bar" | const foo = 'bar' | Parse-time constant |
export FOO="bar" | $env.FOO = 'bar' | Environment variable |
echo $FOO | $env.FOO | Access env var |
echo ${FOO:-default} | `$env.FOO? \ | default 'default'` |
echo $? | $env.LAST_EXIT_CODE | Last exit code |
echo $RANDOM | random int | Random number |
String Operations
| Bash | Nushell |
|---|---|
${var^^} | `$var \ |
${var,,} | `$var \ |
${var:0:5} | `$var \ |
${#var} | `$var \ |
${var/old/new} | `$var \ |
${var//old/new} | `$var \ |
${var%.ext} | `$var \ |
Conditionals
# Bash
if [ "$x" -gt 10 ]; then
echo "big"
elif [ "$x" -gt 5 ]; then
echo "medium"
else
echo "small"
fi# Nushell
if $x > 10 {
'big'
} else if $x > 5 {
'medium'
} else {
'small'
}Loops
# Bash — iterate files
for f in *.txt; do
echo "$f"
done# Nushell — functional
ls *.txt | get name | each {|f| print $f }
# Or simply
ls *.txt | get name# Bash — C-style loop
for ((i=0; i<10; i++)); do
echo $i
done# Nushell
0..9 | each {|i| print $i }
# Or
for i in 0..9 { print $i }# Bash — while loop
while read -r line; do
echo "$line"
done < file.txt# Nushell
open file.txt | lines | each {|line| $line }File Operations
| Bash | Nushell |
|---|---|
cat file | open file or open --raw file |
wc -l file | `open file \ |
touch file | touch file |
mkdir -p dir | mkdir dir |
rm -rf dir | rm -r dir |
cp src dst | cp src dst |
mv src dst | mv src dst |
find . -name "*.rs" | glob **/*.rs |
test -f file | `('file' \ |
test -d dir | `('dir' \ |
basename path | `'path' \ |
dirname path | `'path' \ |
Command Substitution
# Bash
FILES=$(ls *.txt)
COUNT=$(wc -l < file.txt)# Nushell — no special syntax needed
let files = (ls *.txt)
let count = (open file.txt | lines | length)Functions → Custom Commands
# Bash
greet() {
local name="$1"
local greeting="${2:-Hello}"
echo "${greeting}, ${name}!"
}# Nushell
def greet [
name: string
--greeting (-g): string = 'Hello'
]: nothing -> string {
$'($greeting), ($name)!'
}Arrays → Lists
# Bash
arr=(one two three)
echo ${arr[0]}
echo ${#arr[@]}
arr+=("four")# Nushell
let arr = [one two three]
$arr | get 0 # or $arr.0
$arr | length
$arr | append four # Returns new list (immutable)Associative Arrays → Records
# Bash
declare -A config
config[host]="localhost"
config[port]="8080"
echo "${config[host]}"# Nushell
let config = {host: localhost, port: 8080}
$config.hostProcess Management
| Bash | Nushell |
|---|---|
command & | job spawn { command } |
jobs | job list |
kill $PID | kill $pid or job kill $id |
ps aux | ps |
command1 && command2 | command1; command2 |
| `command1 \ | \ |
JSON Processing (jq → Nushell)
# Bash + jq
curl -s api | jq '.users[] | {name, email}'
curl -s api | jq '.items | length'
curl -s api | jq '.data | sort_by(.date)'# Nushell — native structured data
http get api | get users | select name email
http get api | get items | length
http get api | get data | sort-by dateError Handling
# Bash
set -e # Exit on error
trap cleanup EXIT
if ! command; then
echo "Failed" >&2
exit 1
fi# Nushell
try {
some-command
} catch {|err|
print -e $'Failed: ($err.msg)'
exit 1
}Common Patterns
Check if command exists
# Bash
if command -v git &> /dev/null; then echo "found"; fi# Nushell
if (which git | is-not-empty) { print 'found' }Read environment with default
# Bash
PORT="${PORT:-8080}"# Nushell
let port = ($env.PORT? | default 8080)Multiline strings
# Bash heredoc
cat << 'EOF'
line 1
line 2
EOF# Nushell raw string
r#'line 1
line 2'#Nushell Data & Type System Reference
Type Hierarchy
any
├── nothing (null/void)
├── bool
├── int
├── float
├── number (int | float)
├── string
├── datetime
├── duration
├── filesize
├── binary
├── range
├── glob
├── list<T>
├── record<K: V, ...>
├── table<T> (list<record<T>>)
├── closure
├── cell-path
└── errorType Annotations
Function signatures
# No types (accepts any)
def func [x] { ... }
# Typed parameters
def func [x: int, y: string] { ... }
# Pipeline types
def func []: string -> int { ... }
# Both pipeline and parameters
def func [multiplier: int]: list<int> -> list<int> {
$in | each {|x| $x * $multiplier }
}
# Optional parameters with defaults
def func [
x: int
y: int = 10 # Default value
--flag # Named flag (boolean switch)
--option: string # Named param (null if not passed)
--foo: string = 'bar' # Named param with default value
] { ... }Complex types
def func [items: list<string>] { ... }
def func [matrix: list<list<int>>] { ... }
def func [config: record<host: string, port: int>] { ... }
def func [data: table] { ... }
def func [transform: closure] { ... }Type annotations for custom commands
# Shapes valid for parameter annotations
any, binary, bool, cell-path, closure, datetime, duration, filesize,
float, glob, int, list, nothing, number, range, record, string, table
# Special shapes
path # String with ~ and . expansion
directory # Subset of path, only directories for tab-completionBuilt-in Scalar Types
Numbers
42 # int
0xFF # hexadecimal
0o77 # octal
0b1010 # binary
10_000 # underscore separator
3.14 # float
1.5e-3 # scientific notation
inf # infinity
-inf # negative infinity
NaN # not a numberDates and durations
date now # Current datetime
'2024-01-15' | into datetime
'2024-01-15T10:30:00Z' | into datetime
1sec, 5min, 2hr, 3day, 1wk # Duration literals
500ms + 2sec # 2sec 500ms
(date now) + 5day # 5 days from now
(date now) - 1wk # 1 week agoFilesizes
1kb, 500mb, 2gb, 1tb # Filesize literals
1024 | into filesize # 1.0 KiB
100mb + 50mb # 150mb
1gb / 4 # 250mbBinary data
0x[01 FF 3A 00] # Binary literal
'hello' | into binary # String to binary
0x[68656c6c6f] | decode utf-8 # Binary to string
open --raw file.bin # Read raw binaryGlob patterns
glob *.rs # Current dir
glob **/*.rs # Recursive
glob **/*.{rs,toml} # Multiple extensions
glob **/*.rs --exclude [**/target/**] # With exclusions
glob **/*.rs --depth 2 # Max depth
glob '[A-Z]*' --no-file --no-symlink # Only directories
glob '(?i)readme*' # Case-insensitive (wax syntax)Collection Types
Lists
[1 2 3] # list<int>
['Alice' 'Bob'] # list<string>
[[1 2] [3 4]] # list<list<int>>
[] # Empty listRecords
{name: 'Alice', age: 30} # Create
{} # Empty record
{user: {name: 'Alice', contact: {email: 'a@b.com'}}} # NestedTables (list of records)
let users = [
{name: 'Alice', age: 30}
{name: 'Bob', age: 25}
]Ranges
1..5 # Inclusive: [1, 2, 3, 4, 5]
1..<5 # Exclusive end: [1, 2, 3, 4]
1..2..10 # With step: [1, 3, 5, 7, 9]
5..1 # Reverse: [5, 4, 3, 2, 1]
seq char a e # Character sequence: [a, b, c, d, e]Type Conversions
42 | into string # '42'
'42' | into int # 42
3.7 | into int # 3 (truncates)
'3.14' | into float # 3.14
42 | into float # 42.0
'true' | into bool # true
1 | into bool # true
0 | into bool # false
'hello' | into binary # binary representation
'2024-01-15' | into datetime # datetime
1024 | into filesize # 1.0 KiB
60 | into duration --unit sec # 1min
'*.rs' | into glob # glob typeType Checking and Guards
42 | describe # 'int'
[1 2 3] | describe # 'list<int>'
{a: 1} | describe # 'record<a: int>'
# Type guard pattern
def safe-process [value: any] {
match ($value | describe) {
'int' => ($value * 2)
'string' => ($value | str upcase)
_ => null
}
}
# Type predicates
def is-list [] { ($in | describe) starts-with 'list' }Type Coercion Rules
Nushell does NOT auto-coerce types except in string interpolation:
'5' + 3 # TYPE ERROR
$'Value: (42)' # 'Value: 42' (interpolation auto-converts)
42 == '42' # false (different types, no coercion)
42 == ('42' | into int) # true (explicit conversion)Record Operations
$rec1 | merge $rec2 # Merge (right-biased)
[$r1 $r2 $r3] | into record # Merge many records
$rec | update name {|r| $'Dr. ($r.name)' } # Update field
$rec | insert active true # Insert new field
$rec | insert z {|r| $r.x + $r.y } # Computed field
$rec | upsert count {|r| ($r.count? | default 0) + 1 } # Update or insert
$rec | reject password secret_key # Remove fields
$rec | select name age email # Keep only these fields
$rec | items {|k, v| ... } # Iterate key-value pairs
$rec | transpose key val # Convert to tableDynamic field access
let field_name = 'age'
$record | get $field_name # Access by variable
$record | update $field_name 42 # Update by variable
# Dynamic field selection
let fields = if $detailed { [id name email] } else { [id name] }
$table | select ...$fieldsTable Operations
$table | where age > 25 # Filter rows
$table | insert retired {|row| $row.age > 65 } # Add column
$table | rename -c {age: years} # Rename column
$table | group-by status --to-table # Group by
$table | transpose name data # Transpose
# Joins
$users | join $orders user_id # Inner join
$users | join --left $orders user_id # Left join
$users | join --outer $orders user_id # Outer join
# Group-by aggregations
$sales | group-by category --to-table | insert stats {|g| {
count: ($g.items | length)
total: ($g.items | get price | math sum)
avg: ($g.items | get price | math avg)
}}List Operations
$list | enumerate | where {|e| $e.index > 5 } # Filter with index
$list | reduce --fold 0 {|it, acc| $acc + $it } # Accumulate with initial
[1 2 3 4] | reduce {|it, acc| $acc - $it } # Without fold: ((1-2)-3)-4
$list | window 3 # Sliding window
$list | chunks 100 # Batched processing
$list | flatten # Flatten nested lists
$list | skip while {|x| $x < 100 } # Skip while condition true
$list | take while {|x| $x < 1000 } # Take while condition true
$list | uniq # Remove duplicates
$list | sort-by field # Sort
$list | reverse # Reverse orderNull Safety Patterns
$record.field? # null if field missing (no error)
$record.field? | default 'N/A' # Provide fallback
if ($record.field? != null) { ... } # Check existence
$list | default -e $fallback # Default for empty collections
$input | default 'anonymous' # Default for null valuesDiscriminated Unions Pattern
let result = {type: 'success', value: 42}
let error = {type: 'error', message: 'Failed'}
match $result.type {
'success' => $result.value
'error' => { print -e $result.message; null }
}Nushell Modules & Scripts Reference
Module Organization
File-form (simple modules)
my-command.nu # Single-file module, module name = filenameDirectory-form (larger modules)
my-module/
├── mod.nu # Module entry point (required)
├── utils.nu # Submodule
├── config.nu # Submodule
└── tests/
├── mod.nu # Test module entry point
└── utils_test.nu # Test fileBoth forms behave identically once imported; only the path changes.
Export Types
| Export | Keyword | Example |
|---|---|---|
| Commands | export def | export def my-cmd [] { ... } |
| Env commands | export def --env | export def --env setup [] { ... } |
| Aliases | export alias | export alias ll = ls -l |
| Constants | export const | export const version = '1.0.0' |
| Externals | export extern | export extern "git push" [...] |
| Submodules | export module | export module utils.nu |
| Re-exports | export use | export use utils.nu * |
| Env setup | export-env | export-env { $env.FOO = 'bar' } |
Only export-ed definitions are public. Non-exported definitions are private (local to the module).
The main Convention
When a command name matches the module name, use export def main:
# increment.nu
export def main []: int -> int {
$in + 1
}
export def by [amount: int]: int -> int {
$in + $amount
}use increment
5 | increment # => 6
5 | increment by 3 # => 8Submodule Patterns
export module — Preserves submodule namespace
# mod.nu
export module utils.nu # Commands accessed as: my-module utils <cmd>export use — Flattens into parent namespace
# mod.nu
export use utils.nu * # Commands accessed as: my-module <cmd>Environment Setup
# mod.nu
export-env {
$env.MY_MODULE_PATH = ($env.CURRENT_FILE | path dirname)
$env.MY_MODULE_VERSION = '2.0.0'
}Inline Module Definition
module my_module {
export def public-func [] { 'hello' }
def private-func [] { 'private' }
export const MY_CONST = 42
}
use my_module *
use my_module [public-func MY_CONST]Import Patterns
use my-module # Import module namespace
use my-module * # Import all exports into current scope
use my-module [func-a func-b] # Import specific exports
use lib/helpers.nu * # Import from file pathScripts
Basic script
# myscript.nu
def greet [name] {
$'Hello, ($name)!'
}
greet 'World'Definitions run first (regardless of position in file), then the script body runs top-to-bottom.
Parameterized scripts with main
#!/usr/bin/env nu
# Build the project
def "main build" [
--release (-r) # Build in release mode
] {
print 'Building...'
}
# Run tests
def "main test" [
--verbose (-v) # Show test details
] {
print 'Testing...'
}
def main [] {
print 'Usage: script.nu <build|test>'
}nu myscript.nu # => Usage: script.nu <build|test>
nu myscript.nu build # => Building...
nu myscript.nu test # => Testing...Important: You must define a main command for subcommands to be accessible. An empty def main [] {} suffices.
Shebang
#!/usr/bin/env nu
'Hello World!'For stdin access: #!/usr/bin/env -S nu --stdin
Attribute System (v0.103+)
@example 'Greet a user' { greet 'Alice' } --result 'Hello, Alice!'
@deprecated 'Use new-command instead.'
@category 'network'
@search-terms ['http' 'web' 'api']Parse-Time vs Runtime
| Feature | Parse-time | Runtime |
|---|---|---|
const values | Yes | No (already resolved) |
let values | No | Yes |
source / use paths | Must be known | N/A |
| Type checking | Yes | Some |
def names | Must be literal | N/A |
| Syntax errors | Caught here | N/A |
# Works — const is resolved at parse time
const path = 'scripts/utils.nu'
source $path
# Error — let is runtime only
let path = 'scripts/utils.nu'
source $path # Error: not a parse-time constantTesting
Nupm package tests
my-package/
├── nupm.nuon
├── mod.nu
└── tests/
├── mod.nu # Test entry point
└── utils_test.nu # Test fileOnly fully exported commands from tests module are run by nupm test.
Standalone tests with std assert
use std/assert
for t in [
[input expected];
[0 0]
[1 1]
[2 1]
[3 2]
] {
assert equal (fib $t.input) $t.expected
}Available assert commands
use std/assert
assert (condition) # Basic assertion
assert equal $actual $expected # Equality check
assert not equal $a $b # Inequality check
assert str contains $haystack $needle # String containment
assert length $list $expected_len # List length
assert error { failing-command } # Expect an errorCustom assertions
def "assert positive" [n: int] {
assert ($n > 0) --error-label {
text: $'Expected positive number, got ($n)'
span: (metadata $n).span
}
}Basic test framework (without Nupm)
use std/assert
source fib.nu
def main [] {
print 'Running tests...'
let test_commands = (
scope commands
| where ($it.type == 'custom')
and ($it.name | str starts-with 'test ')
and not ($it.description | str starts-with 'ignore')
| get name
| each {|test| [$'print \'Running test: ($test)\'' $test] } | flatten
| str join '; '
)
nu --commands $'source ($env.CURRENT_FILE); ($test_commands)'
print 'Tests completed successfully'
}
def "test fib" [] {
for t in [[input expected]; [0 0] [1 1] [2 1] [5 5]] {
assert equal (fib $t.input) $t.expected
}
}
# ignore
def "test skipped" [] {
print 'This test will not be executed'
}Nushell Script Review Checklist
Comprehensive checklist for reviewing Nushell scripts. Check items in order of priority.
---
1. Security (Critical)
Code injection
- [ ] No
nu -c $variablewith untrusted input - [ ] No
source $variablewith runtime paths (must beconst) - [ ] No
^sh -c,^bash -c, or^cmd.exe /Cwith interpolated user input - [ ] No
run-externalwith user-controlled command names
Path safety
- [ ] User-provided paths validated with
path expand+ prefix check - [ ] No raw
open $user_inputwithout path traversal guard - [ ]
..sequences in user paths detected and rejected - [ ] Base directory enforcement for file operations
Credential handling
- [ ] No hardcoded secrets in source code
- [ ] Credentials scoped with
with-env, not set on$envdirectly - [ ] Secrets read from files/stdin, not passed as command-line arguments
- [ ] No credentials logged via
printor written to non-secure files
Destructive operations
- [ ]
rmoperations validate target path (not/, not$nu.home-path) - [ ] Glob patterns from user input are validated (no unintended expansion)
- [ ]
--depthlimits onglobto prevent DoS on large trees
Temp files
- [ ] Temp files created with
^mktemp, not predictable paths - [ ] Temp files cleaned up in
try/catchor equivalent - [ ] Temp directories use
^mktemp -d
Environment safety
- [ ]
$env.PATHnot modifiable by untrusted input - [ ] Dangerous env vars (
LD_PRELOAD,DYLD_INSERT_LIBRARIES) cleared before running untrusted commands - [ ]
with-envused for scoped environment changes in security-sensitive contexts
---
2. Correctness
Type safety
- [ ] All exported commands have type annotations on parameters
- [ ] I/O pipeline signatures (
]: type -> type {) match actual behavior - [ ] Complex types use proper syntax:
record<name: string>,list<int>,table<col: type> - [ ] Optional parameters use
?suffix:name?: string - [ ] Rest parameters typed:
...args: string
Error handling
- [ ] Fallible operations wrapped in
try/catch - [ ] External commands checked with
completewhen exit code matters - [ ]
catchblocks include meaningful error context (not empty) - [ ] Custom errors include
labelwithspanfor good error messages - [ ] No bare
error make {msg: '...'}without span when metadata is available
Null safety
- [ ] Optional record fields accessed with
?:$rec.field? - [ ]
defaultused for fallback values:$val | default 'N/A' - [ ] No bare field access on records from external/untrusted sources
- [ ]
$incaptured early withletwhen used multiple times
Logic correctness
- [ ]
fornot used as final expression (returns null, useeach) - [ ]
mutvariables not captured in closures (will error) - [ ]
source/usepaths areconst, notlet - [ ]
eachnot used on single records (useitemsortranspose) - [ ] Correct operator:
>in non-pipeline context is comparison, not redirect
External commands
- [ ] External commands prefixed with
^when name conflicts with builtins - [ ]
find(Nushell builtin) vs^find(Unix) distinction maintained - [ ]
sort(Nushell builtin) vs^sort(Unix) distinction maintained - [ ] Arguments to external commands separated (not concatenated strings)
---
3. Style & Idiom
Naming
- [ ] Commands:
kebab-case(fetch-user, notfetchUserorfetch_user) - [ ] Variables/params:
snake_case($user_id, not$userId) - [ ] Env vars:
SCREAMING_SNAKE_CASE($env.APP_VERSION) - [ ] Flags:
kebab-case(--output-dir, not--output_dir) - [ ] Full words preferred (
$user_name, not$usr_nm)
String format priority
- [ ] Bare words in arrays:
[foo bar]not["foo" "bar"] - [ ] Single quotes for simple strings:
'hello'not"hello" - [ ] Single-quoted interpolation preferred:
$'val: ($x)'not$"val: ($x)" - [ ] Double quotes only when escape sequences needed:
"\n","\t" - [ ] Raw strings for regex:
r#'pattern'#
Pipeline & functional style
- [ ] Pipelines preferred over imperative loops
- [ ]
$items | get price | math suminstead ofmut total; for ... - [ ]
ls | where size > 1mbinstead of manual filtering - [ ]
enumerateinstead of manual index counters - [ ]
reduceinstead ofmutaccumulator +for
Formatting
- [ ] No space before
|params|in closures:{|x| ...}not{ |x| ...} - [ ] Spaces around pipe:
cmd | cmdnotcmd|cmd - [ ] Commas omitted in lists:
[1 2 3]not[1, 2, 3] - [ ] One space after
:in records:{x: 1}not{x:1} - [ ] Multi-line format for expressions >80 chars
Documentation
- [ ] Exported commands have
#comment abovedef - [ ] Parameter descriptions as inline
#comments - [ ]
@exampleattributes for non-trivial commands - [ ]
@categoryfor organization when applicable
Modules
- [ ] Only necessary definitions are
export-ed - [ ]
export def mainused when command matches module name - [ ] Private helpers are not exported
- [ ]
export-envfor environment setup blocks
---
4. Performance
Parallelism
- [ ]
par-eachused for I/O-bound work (file reads, HTTP requests) - [ ]
par-eachused for CPU-bound work (data processing) - [ ]
--threadsspecified when controlling concurrency matters - [ ]
eachused only when order matters or list is tiny
Streaming & memory
- [ ]
each --flattenfor streaming nested results - [ ] Large files not loaded entirely when streaming suffices
- [ ]
lines+ pipeline for line-by-line processing of large files - [ ]
first N/take whileto limit processing early
Caching & computation
- [ ] Expensive results cached in
letbindings, not recomputed - [ ]
globwith--depthto avoid scanning huge trees - [ ] Built-in commands preferred over external for small data
- [ ] External tools (
^rg,^jq) used for large-scale operations
---
5. Robustness
Input validation
- [ ] Parameter types annotated (catches misuse at parse time)
- [ ] Range/value checks at function entry for critical params
- [ ] User-facing commands validate inputs before processing
- [ ] Consistent return types (don't mix null and value unexpectedly)
File operations
- [ ]
path existschecked beforeopenwhen file may not exist - [ ]
save --forceused intentionally (overwrites without warning) - [ ] File encoding handled appropriately (
open --rawfor binary)
Process management
- [ ] Long-running external processes have timeouts or cancellation
- [ ] Background jobs (
job spawn) tracked and cleaned up - [ ] Exit codes checked for critical external commands
---
Review Workflow
1. Skim the entire script — Understand purpose, entry points, data flow 2. Security pass — Check Section 1 items systematically 3. Correctness pass — Verify types, error handling, null safety 4. Style pass — Naming, strings, formatting, documentation 5. Performance pass — Parallelism, streaming, caching opportunities 6. Robustness pass — Input validation, file safety, process management 7. Summarize findings — Group by severity, highlight security issues first
Nushell Security Reference
Nushell's Security Model
Built-in safety advantages over Bash
- No `eval` — Code cannot be dynamically generated and executed at runtime
- Static parsing — All code is fully parsed before any evaluation occurs
- Arguments passed as arrays — External command arguments go through
std::process::Command, not through shell interpretation - Type system — Parameter types are checked at parse time, preventing many injection classes
- Scoped environment — Environment changes are local to blocks by default
Remaining attack surfaces
Despite these advantages, Nushell scripts can still be vulnerable to:
- Command injection via
^sh -c,^bash -c, ornu -cwith untrusted input - Path traversal via unvalidated user-provided paths
- Credential leaking through environment variables
- Glob injection from user-controlled patterns
- TOCTOU (Time-of-Check-Time-of-Use) race conditions
- Unsafe temp file creation
- Unhandled external command failures
---
Threat Model
Critical risk
| Threat | Vector | Mitigation |
|---|---|---|
| Code injection | nu -c $user_input | Never pass untrusted input to nu -c or source |
| Shell injection | ^sh -c $untrusted | Never use sh -c/bash -c with interpolated strings |
| Plugin injection | plugin add $untrusted_path | Only install plugins from trusted sources |
High risk
| Threat | Vector | Mitigation |
|---|---|---|
| Path traversal | open $user_path | Validate and canonicalize paths, check against base directory |
| Credential leak | $env.API_KEY = 'secret' | Use with-env for scoped credentials |
| PATH hijacking | $env.PATH poisoning | Use absolute paths for critical commands |
| Glob injection | ^rm $user_pattern | Validate input doesn't contain glob chars, or use --no-expand |
| Env var injection | $env.LD_PRELOAD | Clear dangerous env vars before running untrusted commands |
Medium risk
| Threat | Vector | Mitigation |
|---|---|---|
| TOCTOU | Check then use file | Use atomic operations where possible |
| Temp file race | Predictable /tmp/myfile | Use ^mktemp for unique temp files |
| Unhandled errors | External command silently fails | Use complete and check exit_code |
| Glob DoS | glob **/* on huge trees | Use --depth limits |
| Config tampering | Modified config.nu | Protect config file permissions |
---
Safe Patterns
1. External command execution
# DANGEROUS — shell interprets the entire string
^bash -c $'echo ($user_input)'
^sh -c $user_input
# SAFE — arguments passed directly, no shell interpretation
^echo $user_input
run-external 'ls' '-la' $user_dir
# SAFE — separated command and arguments
let args = [$user_file '--format' 'json']
^cat ...$argsWhy Nushell is safer: When you run ^cmd $arg, Nushell passes $arg as a single argument to the OS process API. It does NOT go through a shell, so ; rm -rf / in $arg is treated as literal text, not a command separator.
The exception: ^sh -c, ^bash -c, ^cmd.exe /C, and nu -c explicitly invoke a shell interpreter, which WILL interpret the string. Never use these with untrusted input.
2. Path validation
# Validate user paths against a base directory
def safe-open [name: string, --base-dir: path = '.'] {
let base = ($base_dir | path expand)
let full = ($base_dir | path join $name | path expand)
# Prevent path traversal
if not ($full | str starts-with $base) {
error make {
msg: 'Path traversal detected'
label: {
text: $'Path ($name) escapes base directory ($base)'
span: (metadata $name).span
}
}
}
# Verify file exists
if not ($full | path exists) {
error make {
msg: $'File not found: ($full)'
label: {text: 'this file', span: (metadata $name).span}
}
}
open $full
}3. Credential handling
# Bad — credential persists in environment, visible to all child processes
$env.DB_PASSWORD = 'hunter2'
^psql -U admin $db_name
# Password is now in env of psql AND any commands after
# Good — scoped credential, only visible within the block
with-env {PGPASSWORD: (open ~/.secrets/db_pass | str trim)} {
^psql -U admin $db_name
}
# PGPASSWORD no longer exists here
# Good — read from file, use directly
let token = (open ~/.config/api-token | str trim)
http get $url -H {Authorization: $'Bearer ($token)'}
# Bad — credential in command line (visible in process listing)
^curl -u $'admin:($password)' $url
# Better — use stdin or config file for credentials
$password | ^tool --password-stdin4. Safe file operations
# Bad — predictable temp file path (race condition)
let tmp = '/tmp/my-script-output'
'data' | save $tmp
# Another process could create/symlink this path first!
# Good — unique temp file via mktemp
let tmp = (^mktemp | str trim)
try {
'data' | save $tmp
# ... process the file ...
} catch {|err|
rm -f $tmp
error make {msg: $err.msg}
}
rm -f $tmp
# Good — unique temp directory
let tmpdir = (^mktemp -d | str trim)5. Safe rm and destructive operations
# Bad — glob from user input, could match anything
^rm -rf $user_provided_path
# Good — validate first
def safe-remove [target: path] {
let resolved = ($target | path expand)
# Never allow removing root or home
if $resolved == '/' or $resolved == $nu.home-dir {
error make {msg: $'Refusing to remove ($resolved)'}
}
# Verify it exists and is expected type
if not ($resolved | path exists) {
error make {msg: $'Path does not exist: ($resolved)'}
}
rm -r $resolved
}6. Glob safety
# Bad — user input could contain glob characters
let pattern = $user_input
glob $pattern # Could expand to unintended files
# Good — escape or validate
def safe-glob [pattern: string, --base-dir: path = '.'] {
# Ensure pattern doesn't escape base directory
if ($pattern | str contains '..') {
error make {msg: 'Pattern must not contain ..'}
}
cd $base_dir
glob $pattern --depth 3 # Limit recursion depth
}7. External command error handling
# Bad — silently ignores failures
^git push origin main
# Good — check exit code
let result = (^git push origin main o+e>| complete)
if $result.exit_code != 0 {
error make {msg: $'git push failed: ($result.stderr)'}
}
# Good — try/catch for simple cases
try {
^cargo test
} catch {
print -e 'Tests failed'
exit 1
}8. Environment variable safety
# Sanitize PATH to prevent command hijacking
def with-safe-path [block: closure] {
with-env {PATH: [/usr/local/bin /usr/bin /bin]} {
do $block
}
}
# Clear dangerous env vars before running untrusted commands
def safe-exec [cmd: string, ...args: string] {
with-env {
LD_PRELOAD: null
LD_LIBRARY_PATH: null
DYLD_INSERT_LIBRARIES: null
} {
run-external $cmd ...$args
}
}---
Windows-Specific Risks
CMD.EXE argument injection
When Nushell calls CMD internal commands on Windows, arguments pass through cmd.exe /D /C:
# These characters are dangerous in CMD context:
# & | < > ^ %
# % expands environment variables: %USERNAME%
# & chains commands: echo hello & whoami
# Nushell blocks \r, \n, and % in CMD arguments (built-in protection)
# But other special characters may still be riskyMitigation
- Avoid CMD internal commands when possible
- Use PowerShell or Nushell native commands instead
- Validate input doesn't contain
&,|,<,>,^
---
Security Review Checklist
When auditing a Nushell script for security:
1. Code injection — Search for nu -c, source, ^sh, ^bash, ^cmd.exe, run-external with user-controlled arguments 2. Path traversal — Search for open, save, rm, cp, mv, glob with user-provided paths; check for .. validation 3. Credentials — Search for $env.*KEY, $env.*SECRET, $env.*PASSWORD, $env.*TOKEN; check if scoped with with-env 4. External commands — Verify complete or try/catch is used for error handling; check for ^ prefix 5. File operations — Check temp file creation uses mktemp; verify rm operations are guarded 6. Glob patterns — Check if user input flows into glob or ls patterns; verify --depth limits 7. Environment — Check if $env.PATH or $env.LD_PRELOAD could be poisoned 8. Error masking — Verify errors are not silently swallowed; check try blocks have meaningful catch
Nushell String Formats Reference
String Format Priority (High to Low)
1. Bare word — Simple word-character-only strings in data contexts 2. Raw string r#'...'# — Regex patterns, paths with quotes, multi-line content 3. Single-quoted '...' — Simple strings without embedded single quotes 4. Single-quoted interpolation $'...' — Interpolation without escape sequences 5. Backtick ` ... — Paths/globs with spaces 6. **Double-quoted** "..." — Only when escape sequences are needed (\n, \t, \", etc.) 7. **Double-quoted interpolation** $"..."` — Only when both interpolation AND escapes are needed
Conversion Rules
Use bare words when:
- Inside arrays:
[foo bar baz]not["foo" "bar" "baz"] - Path join arrays:
[$dir patches]not[$dir "patches"] - Match patterns:
match $x { absolute => ... }notmatch $x { "absolute" => ... }
Use raw strings when:
- Regex patterns with special chars:
r#'(?:a/|b/)?'#not"(?:a/|b/)?" - Strings containing both single and double quotes
- Multi-line content without interpolation
Use single quotes when:
- Simple strings:
'hello world'not"hello world" - No escape sequences or interpolation needed
Use single-quoted interpolation when:
- Variables/expressions present but NO escape sequences:
$'Package: ($pkg.name)'not$"Package: ($pkg.name)"$'Error: ($msg)'not$"Error: ($msg)"
Keep double quotes ONLY when:
- Escape sequences present:
"\n","\t","\r","\"" - Need both interpolation and escapes:
$"Line: ($n)\n"
Important: Single quotes don't escape
In Nushell, \' inside $'...' is NOT an escape — it's a literal backslash + quote.
# Correct — use double quotes when literal single quotes needed
let marker = $"'($pkg)@($ver)':"
# Wrong — backslash doesn't escape in single quotes
let marker = $'\'($pkg)@($ver)\':' # Produces literal backslashes!Important: Command expressions require $ prefix
Strings containing Nushell command expressions wrapped in () MUST keep the $ prefix:
# Correct — $ prefix required for command expressions
print $'(char nl)Done:'
print $'(ansi g)Success!(ansi rst)'
# Wrong — without $ these are literal text
print '(char nl)Done:' # Prints literal "(char nl)"
print '(ansi g)Success!' # Prints literal "(ansi g)"Rule: If a string contains (...) that should be evaluated as a command, always use $'...' or $"...".
String Type Overview
| Format | Syntax | Escapes | Interpolation | Use case |
|---|---|---|---|---|
| Single-quoted | '...' | None | No | Simple strings, Windows paths |
| Double-quoted | "..." | \n \t \" \\ etc. | No | Strings needing escape sequences |
| Raw string | r#'...'# | None | No | Regex, strings with quotes, multi-line |
| Bare word | hello | None | No | Command arguments, list items |
| Backtick | ` ... ` | None | No | Paths/args with spaces, globs |
| Single-interpolated | $'...' | None | Yes | Embedding variables (preferred) |
| Double-interpolated | $"..." | Yes | Yes | Variables + escape sequences |
Examples
Array optimization
# Before
let dirs = [$root, "node_modules", ".pnpm"]
let tools = ["git", "patch"]
# After
let dirs = [$root node_modules .pnpm]
let tools = [git patch]Interpolation optimization
# Before
print $"Package: ($pkg.name)"
print $"Error: ($tool) not found"
# After
print $'Package: ($pkg.name)'
print $'Error: ($tool) not found'Keep double quotes for escapes
# Keep — has \n escape
print $"\nNext steps:"
let content = "line1\nline2"
# Keep — contains literal single quote and interpolation
let marker = $"'($name)@($ver)':"Regex with raw strings
# Before
let pattern = "(?:a/|b/)?"
# After
let pattern = r#'(?:a/|b/)?'#Related skills
How it compares
Pick nushell-pro over generic shell skills when automation must be typed Nushell pipelines optimized for agent editing rather than POSIX bash.
FAQ
What shell does nushell-pro target?
nushell-pro targets Nushell (.nu) scripts and pipelines. The skill helps developers replace bash automation with typed, structured Nushell that coding agents can edit more reliably.
When should developers choose Nushell over bash?
nushell-pro fits data-heavy CLI automation where typed tables and structured pipelines reduce string parsing bugs. POSIX-only environments without Nushell installed are poor fits.
How many installs does nushell-pro have?
nushell-pro reports 6 installs on Skills.sh within hustcer/nushell-craft, reflecting a small focused audience for Nushell agent scripting guidance.