syntaxis/.claude/guidelines/nushell/NUSHELL_0.109_GUIDELINES.md

962 lines
20 KiB
Markdown
Raw Normal View History

# Nushell 0.109+ Guidelines for Syntaxis Project
## Overview
This document provides comprehensive guidelines for writing and maintaining Nushell scripts compatible with Nushell 0.109 and later versions. All scripts in the syntaxis project must follow these standards.
## ⚠️ BREAKING CHANGES in Nushell 0.109+
### Critical: Return Type Annotation Syntax Changed
**OLD SYNTAX (No longer works):**
```nushell
def function_name [param: string] -> string {
$param
}
```
**NEW SYNTAX (Required in 0.109+):**
```nushell
def function_name [param: string]: nothing -> string {
$param
}
```
**Key Changes:**
- Return type annotations now require BOTH input type and output type
- Format: `]: input_type -> output_type`
- Input type specifies what the function accepts from pipeline:
- `nothing` - No pipeline input accepted
- `any` - Accepts any pipeline input (stored in `$in`)
- `string`, `list`, etc. - Specific type required from pipeline
**Migration Required:**
All existing scripts using `-> type` syntax MUST be updated to `]: nothing -> type` or the appropriate input type.
---
## Table of Contents
1. [General Principles](#general-principles)
2. [Script Header & Shebang](#script-header--shebang)
3. [Function Definitions](#function-definitions)
4. [Type Annotations](#type-annotations)
5. [Error Handling](#error-handling)
6. [Closures and Blocks](#closures-and-blocks)
7. [Pipeline and Data Flow](#pipeline-and-data-flow)
8. [String Interpolation](#string-interpolation)
9. [Conditionals](#conditionals)
10. [Loops and Iteration](#loops-and-iteration)
11. [Record and List Operations](#record-and-list-operations)
12. [File System Operations](#file-system-operations)
13. [External Commands](#external-commands)
14. [Module System](#module-system)
15. [Testing](#testing)
16. [Performance Considerations](#performance-considerations)
17. [Common Pitfalls](#common-pitfalls)
---
## General Principles
### ✅ Do's
- **Use type annotations** for function parameters and return types
- **Export public functions** explicitly with `export def`
- **Use descriptive variable names** (snake_case)
- **Leverage the pipeline** for data transformations
- **Handle errors gracefully** with `try-catch`
- **Document complex logic** with comments
- **Use immutable variables** by default; only use `mut` when necessary
### ❌ Don'ts
- **Avoid global mutable state**
- **Don't use deprecated commands** (check Nushell changelog)
- **Don't mix shell commands with Nushell builtins** unnecessarily
- **Avoid deeply nested closures** (prefer helper functions)
- **Don't ignore error cases** in production scripts
---
## Script Header & Shebang
### Standard Header
```nushell
#!/usr/bin/env nu
# script-name.nu - Brief description of script purpose
# Usage: nu script-name.nu [OPTIONS]
#
# Detailed description if needed
```
### With Version Requirement
```nushell
#!/usr/bin/env nu
# Requires Nushell 0.109+
def main [] {
# Verify version
let version = ($nu.version | get major)
if $version < 109 {
error make { msg: "Requires Nushell 0.109+" }
}
}
```
---
## Function Definitions
### Basic Function Syntax
```nushell
# Private function (module scope)
def helper_function [arg: string] -> string {
$arg | str upcase
}
# Public exported function
export def main_function [
arg1: string # Required argument
arg2?: int # Optional argument (note the ?)
--flag: bool = false # Flag with default
--value: string # Named parameter
] -> list<string> {
# Function body
[$arg1, $arg2]
}
```
### Main Entry Point
```nushell
def main [
--verbose: bool = false
...args: string # Rest parameters
] {
if $verbose {
print "Verbose mode enabled"
}
for arg in $args {
print $arg
}
}
```
---
## Type Annotations
### Supported Types
```nushell
# Scalar types
def example [
text: string
number: int
decimal: float
flag: bool
date: datetime
duration: duration
filesize: filesize
] {}
# Collection types
def collections [
items: list<string> # Homogeneous list
config: record # Record (struct)
table: table # Table type
any_list: list # List of any type
] {}
# Optional and nullable
def optional [
maybe_text?: string # Optional parameter
] {}
# Return type annotations (NEW SYNTAX in 0.109+)
# Format: def name [params]: input_type -> output_type { body }
def get_items []: nothing -> list<string> {
["item1", "item2"]
}
def get_config []: nothing -> record {
{name: "test", version: "1.0"}
}
# Function that accepts pipeline input
def process_items []: list<string> -> list<string> {
$in | each {|item| $item | str upcase}
}
```
---
## Error Handling
### Try-Catch Pattern
```nushell
# Basic try-catch
def safe_operation [] {
try {
# Risky operation
open file.txt
} catch { |err|
# Handle error
print $"Error: ($err.msg)"
return null
}
}
# Try with default value
def get_value_or_default [] -> string {
try {
open config.toml | get key
} catch {
"default_value"
}
}
```
### Error Creation
```nushell
def validate_input [value: int] {
if $value < 0 {
error make {
msg: "Value must be non-negative"
label: {
text: "invalid value"
span: (metadata $value).span
}
}
}
}
```
### Null Handling
```nushell
# Use null-coalescing with default operator
def get_env_or_default [key: string, default: string] -> string {
$env | get -i $key | default $default
}
# Safe navigation with optional chaining
def get_nested_value [data: record] -> any {
$data | get -i outer.inner.value | default null
}
```
---
## Closures and Blocks
### Closure Syntax (Nushell 0.109+)
```nushell
# Simple closure
let double = {|x| $x * 2}
[1, 2, 3] | each $double
# Multi-line closure
let process = {|item|
let result = $item | str upcase
$result + "!"
}
# Closure with pipeline input ($in)
let items = [1, 2, 3] | where {|it| $it > 1}
# IMPORTANT: Use {|it| ...} not { $in | ... } for newer versions
```
### Block vs Closure
```nushell
# Block (no parameters, uses $in)
def process_with_block [] {
[1, 2, 3] | each { $in * 2 } # Uses $in implicitly
}
# Closure (explicit parameters preferred)
def process_with_closure [] {
[1, 2, 3] | each {|x| $x * 2} # Explicit parameter
}
```
### Common Patterns
```nushell
# Filter with closure
let adults = $people | where {|person| $person.age >= 18}
# Map transformation
let names = $users | each {|user| $user.name}
# Reduce/fold
let sum = [1, 2, 3, 4] | reduce {|acc, val| $acc + $val}
# Any/all predicates
let has_errors = $items | any {|item| $item.status == "error"}
let all_valid = $items | all {|item| $item.valid}
```
---
## Pipeline and Data Flow
### Pipeline Best Practices
```nushell
# ✅ Good: Clear pipeline flow
def process_data [] {
open data.json
| get items
| where {|item| $item.active}
| each {|item| {name: $item.name, count: $item.count}}
| sort-by count
}
# ❌ Avoid: Unnecessary intermediate variables
def process_data_bad [] {
let data = open data.json
let items = $data | get items
let filtered = $items | where {|item| $item.active}
# ... prefer pipeline instead
}
```
### Pipeline Input Variable
```nushell
# $in represents pipeline input
def uppercase_all [] {
["hello", "world"] | each { $in | str upcase }
}
# Prefer explicit parameters for clarity
def uppercase_all_better [] {
["hello", "world"] | each {|text| $text | str upcase}
}
```
---
## String Interpolation
### String Formatting
```nushell
# Basic interpolation
let name = "World"
print $"Hello, ($name)!"
# Expression interpolation
let count = 5
print $"You have ($count * 2) items"
# Nested interpolation
let user = {name: "Alice", age: 30}
print $"User: ($user.name), Age: ($user.age)"
# Multi-line strings
let message = $"
Welcome to syntaxis
Version: (open Cargo.toml | get package.version)
Date: (date now | format date "%Y-%m-%d")
"
```
### ANSI Colors
```nushell
# Using ansi command
print $"(ansi cyan)INFO:(ansi reset) Processing..."
print $"(ansi red)ERROR:(ansi reset) Failed"
print $"(ansi green)✓(ansi reset) Success"
# Common patterns
def print_success [msg: string] {
print $"(ansi green)✓(ansi reset) ($msg)"
}
def print_error [msg: string] {
print $"(ansi red)✗(ansi reset) ($msg)"
}
def print_info [msg: string] {
print $"(ansi cyan)(ansi reset) ($msg)"
}
```
---
## Conditionals
### If-Else Statements
```nushell
# Basic if-else
def check_value [x: int] -> string {
if $x > 0 {
"positive"
} else if $x < 0 {
"negative"
} else {
"zero"
}
}
# Inline if (expression form)
def abs [x: int] -> int {
if $x >= 0 { $x } else { -$x }
}
```
### Match Expressions
```nushell
# Pattern matching
def process_status [status: string] -> string {
match $status {
"pending" => "⏳ Waiting"
"running" => "▶️ In Progress"
"completed" => "✅ Done"
"failed" => "❌ Error"
_ => "❓ Unknown"
}
}
# Match with guards
def categorize [num: int] -> string {
match $num {
$x if $x < 0 => "negative"
0 => "zero"
$x if $x < 10 => "small"
$x if $x < 100 => "medium"
_ => "large"
}
}
```
---
## Loops and Iteration
### For Loops
```nushell
# Basic for loop
def process_items [] {
for item in [1, 2, 3] {
print $"Processing: ($item)"
}
}
# With index using enumerate
def indexed_loop [] {
for (idx, val) in ([1, 2, 3] | enumerate) {
print $"Index ($idx): ($val)"
}
}
```
### While Loops
```nushell
# While loop
def countdown [mut n: int] {
while $n > 0 {
print $n
$n = $n - 1
}
print "Done!"
}
```
### Preferred: Functional Iteration
```nushell
# Use each instead of for when transforming
def double_all [items: list<int>] -> list<int> {
$items | each {|x| $x * 2}
}
# Use where for filtering
def get_active [items: list] -> list {
$items | where {|item| $item.active}
}
# Use reduce for aggregation
def sum [numbers: list<int>] -> int {
$numbers | reduce {|acc, val| $acc + $val}
}
```
---
## Record and List Operations
### Record Operations
```nushell
# Create record
let config = {
name: "syntaxis"
version: "0.1.0"
features: ["cli", "tui", "api"]
}
# Access fields
let name = $config.name
let version = $config | get version
# Safe access (returns null if missing)
let missing = $config | get -i missing_key
# Update record
let updated = $config | insert new_field "value"
let modified = $config | update version "0.2.0"
# Remove field
let smaller = $config | reject features
# Merge records
let merged = $config | merge {author: "Akasha"}
# Check if key exists
if "name" in $config {
print "Has name field"
}
```
### List Operations
```nushell
# Create list
let items = [1, 2, 3, 4, 5]
# Access elements
let first = $items | first
let last = $items | last
let at_index = $items | get 2
# Add elements
let appended = $items | append 6
let prepended = $items | prepend 0
# Filter
let evens = $items | where {|x| $x mod 2 == 0}
# Map
let doubled = $items | each {|x| $x * 2}
# Reduce
let sum = $items | reduce {|acc, val| $acc + $val}
# Sort
let sorted = $items | sort
let sorted_desc = $items | sort --reverse
# Unique
let unique = [1, 2, 2, 3] | uniq
# Flatten
let flat = [[1, 2], [3, 4]] | flatten
# Zip
let pairs = [$items, $doubled] | zip
# Take/skip
let first_three = $items | take 3
let skip_two = $items | skip 2
# Check membership
if 3 in $items {
print "Found"
}
# Length
let count = $items | length
```
---
## File System Operations
### Path Operations
```nushell
# Check if path exists
def file_exists [path: string] -> bool {
$path | path exists
}
# Path manipulation
let expanded = "~/config" | path expand
let joined = ["configs", "database.toml"] | path join
let dirname = "/path/to/file.txt" | path dirname
let basename = "/path/to/file.txt" | path basename
let extension = "file.txt" | path extension
# Path type checking
if ($path | path exists) and ($path | path type) == "dir" {
print "Is a directory"
}
```
### File Operations
```nushell
# Read file
def read_config [path: string] -> record {
if not ($path | path exists) {
return {}
}
try {
open $path
} catch {
print $"Warning: Could not read ($path)"
{}
}
}
# Write file
def save_config [config: record, path: string] {
try {
$config | to toml | save --force $path
print $"✓ Saved to ($path)"
} catch { |err|
print $"✗ Failed to save: ($err.msg)"
}
}
# Directory operations
def ensure_dir [path: string] {
if not ($path | path exists) {
mkdir $path
}
}
# List files with glob
def find_nu_scripts [] -> list<string> {
glob **/*.nu
}
```
---
## External Commands
### Running External Commands
```nushell
# Use ^ prefix for external commands
def run_cargo_check [] {
^cargo check --workspace
}
# Capture output
def get_git_branch [] -> string {
^git branch --show-current | str trim
}
# Check command existence
def has_command [cmd: string] -> bool {
which $cmd | length > 0
}
# Conditional command execution
def maybe_run [cmd: string] {
if (has_command $cmd) {
^$cmd --version
} else {
print $"Command not found: ($cmd)"
}
}
# Suppress errors with try
def silent_command [] {
try {
^some-command 2>/dev/null
} catch {
null
}
}
```
### Shell Redirection
```nushell
# Redirect stderr
^command 2>/dev/null
# Redirect both stdout and stderr
^command 2>&1
# Pipe to file
^command | save output.txt
# Append to file
^command | save --append output.txt
```
---
## Module System
### Module Structure
```nushell
# my-module.nu
export def public_function [] {
print "Public"
}
def private_function [] {
print "Private"
}
export def another_public [] {
private_function # Can call private functions
}
```
### Importing Modules
```nushell
# Import all exports
use my-module.nu
# Import specific functions
use my-module.nu [public_function, another_public]
# Import with alias
use my-module.nu [public_function as pub_fn]
# Relative imports
use ../common/utils.nu [helper]
use ./local-module.nu *
```
### Standard Library
```nushell
# Use standard library modules
use std log
use std assert
def example [] {
log info "Starting process"
log debug "Debug information"
assert equal 1 1 # Unit test assertion
}
```
---
## Testing
### Test Functions
```nushell
# Test annotation
#[test]
def test_addition [] {
assert equal (1 + 1) 2
}
#[test]
def test_string_operations [] {
let result = "hello" | str upcase
assert equal $result "HELLO"
}
# Test with error handling
#[test]
def test_error_case [] {
assert error { error make { msg: "test error" } }
}
```
### Integration Tests
```nushell
# test-integration.nu
use std assert
def main [] {
print "Running integration tests..."
test_config_loading
test_file_operations
print "All tests passed!"
}
def test_config_loading [] {
let config = load_config "test-config.toml"
assert ($config.name == "test")
}
def test_file_operations [] {
let temp_file = "temp-test.txt"
"test content" | save $temp_file
assert ($temp_file | path exists)
rm $temp_file
}
```
---
## Performance Considerations
### Efficient Data Processing
```nushell
# ✅ Good: Use pipeline for streaming
def process_large_file [] {
open --raw large.txt
| lines
| where {|line| $line | str contains "ERROR"}
| each {|line| parse_error_line $line}
}
# ❌ Avoid: Loading everything into memory
def process_large_file_bad [] {
let all_lines = open large.txt | lines
let errors = $all_lines | where {|line| $line | str contains "ERROR"}
# Processes entire file at once
}
```
### Lazy Evaluation
```nushell
# Take advantage of lazy evaluation
def find_first_match [pattern: string] {
glob **/*.txt
| each { open }
| where {|content| $content | str contains $pattern}
| first # Stops after first match
}
```
### Avoid Unnecessary Conversions
```nushell
# ✅ Good: Work with structured data
def get_names [] {
open users.json
| get users
| each {|user| $user.name}
}
# ❌ Avoid: Converting to strings unnecessarily
def get_names_bad [] {
open users.json
| to text # Unnecessary conversion
| from json
| get users
}
```
---
## Common Pitfalls
### Pitfall 1: Closure vs Block Confusion
```nushell
# ❌ Wrong: Using $in in closure parameter context
let bad = [1, 2, 3] | where { $in > 1 } # May not work in newer versions
# ✅ Correct: Use explicit parameter
let good = [1, 2, 3] | where {|x| $x > 1}
```
### Pitfall 2: Mutable Variable Scope
```nushell
# ❌ Wrong: Trying to mutate outer scope
def increment_bad [] {
let mut counter = 0
[1, 2, 3] | each {|x|
$counter = $counter + 1 # Error: can't mutate outer scope
}
}
# ✅ Correct: Use reduce or return values
def increment_good [] -> int {
[1, 2, 3] | reduce {|acc, val| $acc + 1} --fold 0
}
```
### Pitfall 3: Missing Error Handling
```nushell
# ❌ Risky: No error handling
def load_config [] {
open config.toml | get database.url # Crashes if missing
}
# ✅ Safe: Handle errors gracefully
def load_config_safe [] -> string {
try {
open config.toml | get database.url
} catch {
print "Warning: Using default database URL"
"sqlite::memory:"
}
}
```
### Pitfall 4: Type Mismatches
```nushell
# ❌ Wrong: Implicit type assumption
def add [a, b] { # No type annotations
$a + $b # May fail if wrong types
}
# ✅ Correct: Explicit types
def add [a: int, b: int] -> int {
$a + $b
}
```
### Pitfall 5: Path Handling
```nushell
# ❌ Wrong: Hardcoded paths
def get_config [] {
open /Users/username/config.toml # Not portable
}
# ✅ Correct: Use environment variables and path expansion
def get_config [] -> record {
let config_path = $"($env.HOME)/.config/syntaxis/config.toml"
if ($config_path | path exists) {
open $config_path
} else {
{}
}
}
```
### Pitfall 6: Ignoring Null Values
```nushell
# ❌ Wrong: Assumes value exists
def get_value [data: record] {
$data.key.nested.value # Crashes if any key missing
}
# ✅ Correct: Safe navigation
def get_value_safe [data: record] -> any {
$data | get -i key.nested.value | default null
}
```
---
## Migration Checklist for Existing Scripts
When updating scripts to Nushell 0.109+:
- [ ] **CRITICAL:** Update return type syntax from `] -> type {` to `]: nothing -> type {`
- [ ] Update closures to use explicit parameters `{|x| ...}` instead of `{ $in ... }`
- [ ] Add type annotations to function parameters and return types
- [ ] Replace deprecated commands (check Nushell changelog)
- [ ] Add proper error handling with try-catch
- [ ] Use `export def` for public functions
- [ ] Add documentation comments
- [ ] Update file operations to use proper path checking
- [ ] Replace string-based path handling with path commands
- [ ] Add tests for critical functions
- [ ] Verify null handling with `-i` flag and `default`
- [ ] Check external command calls use `^` prefix
- [ ] Ensure ANSI color usage is consistent
- [ ] Change `let mut` to `mut` for mutable variables
---
## References
- [Nushell Official Documentation](https://www.nushell.sh/book/)
- [Nushell Language Guide](https://www.nushell.sh/lang-guide/)
- [Nushell Cookbook](https://www.nushell.sh/cookbook/)
- [Nushell Release Notes](https://github.com/nushell/nushell/releases)
---
**Last Updated**: 2025-12-01
**Minimum Version**: Nushell 0.109+
**Maintained by**: syntaxis Development Team