PSA/docs/reference/nushell_cheatsheet.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

14 KiB

Nushell Development Cheatsheet

This document covers common gotchas and syntax differences when developing in Nushell, particularly for developers coming from bash or other traditional shells. These lessons learned come from building the Alga PSA CLI tool.

Table of Contents

  1. String Interpolation
  2. Variable Scope and Capture Issues
  3. Background Process Handling
  4. Parentheses Escaping
  5. Function Parameter Handling
  6. Path Handling
  7. Command Output and Error Handling
  8. General Syntax Differences

String Interpolation

The Problem

Nushell has different string interpolation syntax than bash, and mixing them up leads to confusing errors.

Solution Patterns

Correct Nushell String Interpolation

# Use $"..." for interpolated strings with parentheses around variables
let name = "world"
print $"Hello ($name)!"

# For nested interpolation:
let pr_number = 123
let namespace = $"alga-pr-($pr_number)"
print $"($color_cyan)Creating environment for PR ($pr_number) in namespace ($namespace)($color_reset)"

Wrong Patterns (Bash-style)

# DON'T use bash-style $variable inside strings
print "Hello $name!"  # Won't interpolate

# DON'T use ${variable} syntax
print "Hello ${name}!" # Syntax error

Different Variable Reference Types

# $variable - Direct variable reference (no interpolation)
let my_var = some_value

# $(expression) - Command substitution 
let current_time = $(date now)

# $"string with (variable)" - String interpolation
let message = $"Current time is (date now)"

Variable Scope and Capture Issues

The Problem

Variables declared in outer scopes can have capture issues when used in closures or do blocks.

Solution: Copy Variables to Avoid Capture Issues

Correct Pattern

def find-project-root [] {
    let current_dir = pwd
    mut search_dir = $current_dir
    
    loop {
        # Copy to avoid capture issue in closure
        let current_search_dir = $search_dir  
        let has_indicators = ($root_indicators | all { |indicator| 
            ($current_search_dir | path join $indicator | path exists)
        })
        
        # ... rest of logic
    }
}

Problematic Pattern

loop {
    # Direct use of mut variable in closure can cause issues
    let has_indicators = ($root_indicators | all { |indicator| 
        ($search_dir | path join $indicator | path exists)  # Capture issue!
    })
}

Variable Mutation

# Use 'mut' for variables that will change
mut search_dir = $current_dir
$search_dir = ($search_dir | path dirname)

# Use 'let' for immutable variables
let project_root = find-project-root

Background Process Handling

The Problem

Nushell's & operator doesn't work like bash's backgrounding operator.

Solution: Use bash -c for Background Processes

Correct Pattern (Fallback to Bash)

# Use bash -c for proper backgrounding
print "Starting port forwarding processes..."
bash -c $"kubectl port-forward -n ($namespace) svc/service1 8080:8080 &"
bash -c $"kubectl port-forward -n ($namespace) svc/service2 3001:3000 &"

# Kill background processes
bash -c $"pkill -f 'kubectl port-forward.*dev-env-pr-($pr_number)'"

Wrong Pattern (Nushell Native)

# This doesn't work the same way as bash
kubectl port-forward -n $namespace svc/service1 8080:8080 &  # Won't background properly

Alternative: Using do with Background Tasks

# For some cases, you can use 'do' blocks with job management
let result = do {
    cd $project_root
    docker compose up -d | complete
}

Parentheses Escaping

The Problem

Parentheses in strings need to be escaped when they're not part of variable interpolation.

Solution: Escape Parentheses in Display Text

Correct Pattern

# Escape parentheses that are meant to be literal
print $"($color_cyan)Local Access \(Port Forward\):($color_reset)"
print $"  Connect: dev-env-connect ($pr_number) [--port-forward] [--code-server]"

# Or use different punctuation to avoid the issue entirely
print $"($color_yellow)Helm completed with warnings - ignoring:($color_reset)"

# Escape parentheses around words that could be commands
print $"  PSA App \(in code\):  http://localhost:3002"

Wrong Pattern

# Unescaped parentheses may be interpreted as interpolation
print $"($color_cyan)Local Access (Port Forward):($color_reset)"  # May cause issues

# This will try to execute 'ignoring' as a command!
print $"($color_yellow)Helm completed with warnings (ignoring):($color_reset)"  # ERROR!

# This will try to execute 'in' as a command!
print $"  PSA App (in code):  http://localhost:3002"  # ERROR!

Function Parameter Handling

The Problem

Nushell requires --wrapped for functions that need to handle dynamic arguments with flags.

Solution: Use --wrapped for CLI Argument Parsing

Correct Pattern

# Use --wrapped for main function that processes CLI args
def --wrapped main [
   ...args: string   # All arguments and flags as strings
] {
   let command = ($args | get 0? | default null)
   let command_args = ($args | skip 1)
   
   # Parse flags manually
   let detached = ($command_args | any { |arg| $arg == "--detached" or $arg == "-d" })
   let edition_idx = ($command_args | enumerate | where {|item| $item.item == "--edition"} | get 0?.index | default null)
}

Wrong Pattern

# Without --wrapped, flag parsing is problematic
def main [
   command?: string,
   arg2?: string  # Can't handle dynamic flags
] {
   # Limited argument handling
}

Optional Parameters and Defaults

# Use '?' for optional parameters and 'default' for fallbacks
def dev-env-status [
    pr_number?: int    # Optional parameter
] {
    if ($pr_number | is-empty) {
        dev-env-list
        return
    }
    # ... rest of function
}

# Use default values in parameter definitions
def dev-up [
    --detached (-d) # Boolean flag
    --edition (-e): string = "ce" # String with default value
] {
    # Function body
}

Path Handling

The Problem

Path operations in Nushell use a different API than traditional shells.

Solution: Use Nushell Path Commands

Correct Pattern

# Use path commands for file operations
let project_root = find-project-root
let workflow_file = ($project_root | path join "server" "src" "lib" "workflows" $"($workflow_name).ts")

# Check if path exists
if not ($workflow_file | path exists) {
    error make { msg: $"File not found: ($workflow_file)" }
}

# Get parent directory
let parent = ($search_dir | path dirname)

Wrong Pattern (Bash-style)

# Don't use bash-style path operations
let workflow_file = "$project_root/server/src/lib/workflows/$workflow_name.ts"  # Won't interpolate

Command Output and Error Handling

The Problem

Nushell handles command output and errors differently than bash.

Solution: Use complete and Proper Error Handling

Correct Pattern

# Use 'complete' to capture both stdout and stderr
let result = do {
    cd ($project_root | path join "server")
    npx knex migrate:up --knexfile knexfile.cjs --env migration | complete
}

# Check exit code and handle output
if $result.exit_code == 0 {
    print $result.stdout
    print $"($color_green)Migration completed successfully.($color_reset)"
} else {
    print $"($color_red)($result.stderr)($color_reset)"
    error make { msg: $"Migration failed", code: $result.exit_code }
}

Error Creation

# Use 'error make' for custom errors
error make { 
    msg: $"($color_red)Unknown command: '($command)'($color_reset)" 
}

# With error codes
error make { 
    msg: $"($color_red)Migration failed($color_reset)", 
    code: $result.exit_code 
}

Boolean Expression Issues

The Problem

Complex boolean expressions with and, or, and not operators can cause "incomplete math expression" or "can't convert to cell-path" errors when combined with pipeline operations.

Solution: Break Down Complex Boolean Logic

Correct Pattern

# Break complex boolean expressions into separate variables
let has_warnings = ($stderr_content | str contains 'warning:')
let has_errors = ($stderr_content | str contains 'error:')
let is_warnings_only = ($has_warnings and not $has_errors)

# Use separate conditions for or operations
let has_terminating = ($stuck_pods.stdout | str contains 'Terminating')
let has_pending = ($stuck_pods.stdout | str contains 'Pending')
if $stuck_pods.exit_code == 0 and ($has_terminating or $has_pending) {
    # Handle stuck pods
}

# Chain multiple where clauses instead of complex and conditions
let filtered_stderr = ($helm_result.stderr | lines | where { |line| 
    (not ($line | str contains 'Kubernetes configuration file is'))
} | where { |line|
    (not ($line | str contains 'deprecated since'))
} | where { |line|
    (($line | str trim | str length) > 0)
})

Wrong Pattern

# Complex boolean expressions in single line cause errors
let is_warnings_only = ($stderr_content | str contains 'warning:' and not ($stderr_content | str contains 'error:'))  # ERROR!

# Multiple and/or operations in single where clause
let filtered_stderr = ($helm_result.stderr | lines | where { |line| 
    (not ($line | str contains 'Kubernetes configuration file is')) and 
    (not ($line | str contains 'deprecated since')) and
    (($line | str trim | str length) > 0)
})  # ERROR!

# Complex or operations with pipelines
if $stuck_pods.exit_code == 0 and ($stuck_pods.stdout | str contains 'Terminating' or $stuck_pods.stdout | str contains 'Pending') {  # ERROR!
    # This causes "incompatible path access" error
}

General Syntax Differences

Conditional Checks

# String length checks
if ($line | str trim | str length) > 0 {
    # Process non-empty line
}

# Check if value is in list
if not ($edition in ["ce", "ee"]) {
    error make { msg: "Invalid edition" }
}

# Check for empty values
if ($pr_number | is-empty) {
    return
}

Data Processing Pipelines

# Use pipeline operations for data transformation
open $env_path
| lines
| each { |line| $line | str trim }
| where { |line| not ($line | str starts-with '#') and ($line | str length) > 0 }
| split column "=" -n 2
| rename key value
| reduce -f {} { |item, acc| $acc | upsert $item.key $item.value }

Environment Variables

# Set environment for command execution
with-env { PGPASSWORD: $db_env.DB_PASSWORD_ADMIN } {
    $sql_update | psql -h $db_env.DB_HOST -p $db_env.DB_PORT -U $db_env.DB_USER_ADMIN -d $db_env.DB_NAME_SERVER -f -
}

Match Statements

# Use match for multiple conditions
match $action {
    "up" => {
        print "Running migration up..."
        # migration logic
    }
    "down" => {
        print "Running migration down..."
        # revert logic
    }
    _ => {
        error make { msg: $"Unknown action: ($action)" }
    }
}

Key Takeaways for Bash Users

  1. String interpolation: Use $"text (variable) more text" instead of "text $variable more text"
  2. Variables: Use let for immutable, mut for mutable, copy variables to avoid closure capture issues
  3. Background processes: Fall back to bash -c for proper backgrounding when needed
  4. Parentheses: Escape literal parentheses as \( and \) in interpolated strings
  5. Command output: Use | complete to capture both stdout and stderr
  6. Path operations: Use path join, path exists, path dirname instead of string concatenation
  7. Function parameters: Use --wrapped and ...args for flexible CLI argument handling
  8. Error handling: Use error make instead of exit or return with error codes

Common Debug Tips

  1. Check variable values: Use print $variable to debug variable contents
  2. Test string interpolation: Start simple with $"Hello (variable)" patterns
  3. Pipeline debugging: Add | debug to see data flowing through pipelines
  4. Command testing: Test commands with | complete to see full output structure
  5. Scope issues: If variables aren't accessible in closures, copy them to local variables first

Alga PSA CLI Commands

Common Commands

# Database migrations
migrate up      # Run next pending migration
migrate latest  # Run all pending migrations  
migrate down    # Revert last migration
migrate status  # Check migration status

# Development environment
dev-up --edition ce --detached  # Start local dev environment
dev-down                        # Stop local dev environment

# Branch-based development environments
dev-env-create feature-branch --use-latest
dev-env-list                    # List all dev environments
dev-env-connect feature-branch --port-forward
dev-env-destroy feature-branch --force
dev-env-status feature-branch

# Workflow management
update-workflow invoice-sync    # Update workflow definition
register-workflow customer-sync # Register new workflow

# Build Docker images
build-image ce --tag v1.0.0 --push
build-image ee --use-latest --push
build-all-images --tag latest --push

# Build code-server image via CLI
build-code-server --push
build-code-server --tag v1.0.0 --push
build-code-server --use-latest --push

# Build code-server image (alternative from docker/dev-env directory)
./build-code-server.sh [TAG]

This cheatsheet covers the main gotchas encountered while developing the Alga PSA CLI. When in doubt, check the official Nushell documentation for more detailed explanations.