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)
20 KiB
20 KiB
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 acceptedany- 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
- General Principles
- Script Header & Shebang
- Function Definitions
- Type Annotations
- Error Handling
- Closures and Blocks
- Pipeline and Data Flow
- String Interpolation
- Conditionals
- Loops and Iteration
- Record and List Operations
- File System Operations
- External Commands
- Module System
- Testing
- Performance Considerations
- 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
mutwhen 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 deffor 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
-iflag anddefault - Check external command calls use
^prefix - Ensure ANSI color usage is consistent
- Change
let muttomutfor mutable variables
References
Last Updated: 2025-12-01 Minimum Version: Nushell 0.109+ Maintained by: syntaxis Development Team