syntaxis/.claude/guidelines/nushell/NUSHELL_0.109_GUIDELINES.md
Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
Merge _configs/ into config/ for single configuration directory.
Update all path references.

Changes:
- Move _configs/* to config/
- Update .gitignore for new patterns
- No code references to _configs/ found

Impact: -1 root directory (layout_conventions.md compliance)
2025-12-26 18:36:23 +00:00

20 KiB
Raw Blame 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):

def function_name [param: string] -> string {
    $param
}

NEW SYNTAX (Required in 0.109+):

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
  2. Script Header & Shebang
  3. Function Definitions
  4. Type Annotations
  5. Error Handling
  6. Closures and Blocks
  7. Pipeline and Data Flow
  8. String Interpolation
  9. Conditionals
  10. Loops and Iteration
  11. Record and List Operations
  12. File System Operations
  13. External Commands
  14. Module System
  15. Testing
  16. Performance Considerations
  17. 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

#!/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

#!/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

# 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

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

# 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

# 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

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

# 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+)

# 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

# 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

# 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

# ✅ 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

# $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

# 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

# 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

# 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

# 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

# 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

# While loop
def countdown [mut n: int] {
    while $n > 0 {
        print $n
        $n = $n - 1
    }
    print "Done!"
}

Preferred: Functional Iteration

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# ✅ 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

# 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

# ✅ 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

# ❌ 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

# ❌ 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

# ❌ 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

# ❌ 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

# ❌ 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

# ❌ 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


Last Updated: 2025-12-01 Minimum Version: Nushell 0.109+ Maintained by: syntaxis Development Team