# 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 { # 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 # 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 { ["item1", "item2"] } def get_config []: nothing -> record { {name: "test", version: "1.0"} } # Function that accepts pipeline input def process_items []: list -> list { $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] -> list { $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 { $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 { 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