Skip to content
Shell & TerminalDeep Dive Published Updated 4 min readViews unavailable

Nushell Custom Commands: Parameter Types, Pipeline Contracts, and Validation

Use Nushell signatures to document argument and pipeline types, catch known mismatches, and add explicit checks for real-world constraints.

A Nushell custom command has more than one boundary. Its positional parameters and flags define what callers may pass by name. Its pipeline signature describes the input and output types that the command supports. Neither contract, by itself, says whether a value is meaningful for the operation: an integer port can still be outside the valid TCP range, and a string path can still name a missing file.

Keeping those layers separate makes commands easier to compose and safer to reuse. Give stable data shapes explicit types, describe pipeline behavior when it is known, and validate domain rules in the command body. Do not mistake a useful signature for proof that every dynamic value or external input has been validated.

Type the arguments callers can control

An unannotated parameter has type any. Add an annotation when a command genuinely needs a particular kind of value. Nushell supports basic types as well as compound forms such as list<int>, shaped records, and unions such as oneof<int, string>. The same idea applies to named flags.

def sum-values [values: list<int>] {
    $values | math sum
}

sum-values [12 4 80]

The parser can reject a known mismatch while the command is being entered or parsed; an annotation is not merely a comment for the help page. It also makes the command’s expected interface visible through help and command introspection. But any values, data loaded from files, and values crossing external-process boundaries may not have the same static information available. Inspect or validate those values when correctness depends on more than their broad Nushell type.

Prefer the narrowest type that describes the actual input contract. A command that accepts any table should say table; use a shaped record only when the required fields and their types are truly part of the interface. An annotation cannot make a loosely structured input stable by wishful thinking.

Declare pipeline input and output separately

Argument types and pipeline types solve different problems. A command can take no positional arguments and still consume pipeline input. Conversely, a command can take a path argument while ignoring its pipeline. Without an explicit input-output signature, custom commands default to any pipeline input and any output.

def bump [by: int = 1]: int -> int {
    $in + $by
}

41 | bump
# => 42

Here by is a typed command argument, while int -> int says that the filter accepts an integer from the pipeline and produces an integer. The default lets a caller omit by, but does not change the pipeline contract. A known incompatible pipeline such as a string can be reported as an input-type mismatch; a body that produces a value outside the declared output type is also a contract error.

When one command deliberately supports several input shapes, Nushell lets a signature list multiple input-output pairs. Treat those pairs as the allowed contract for the same command body, not as a substitute for separate commands when the behavior itself should differ. If the body performs conversions, make those conversions visible and check the output type that callers should actually rely on.

Add semantic checks after type checks

A type annotation can prove that a port is an integer; it cannot prove that the number is a valid port. Put that application rule in the command and report a useful error at the boundary:

def checked-port [port: int]: nothing -> int {
    if $port < 1 or $port > 65535 {
        error make {msg: "port must be in the range 1..65535"}
    }
    $port
}

checked-port 443
# => 443

checked-port 70000
# => error: port must be in the range 1..65535

The parameter type rejects a non-integer argument; the explicit range check rejects an integer that is invalid for this operation. This is the same distinction to preserve for paths, durations, identifiers, table columns, and configuration loaded at runtime. A declared type is a structural contract, not a business-rule engine.

Test both sides of the contract

For a new command, try one valid value, a value of the wrong type, and a correctly typed value that violates the semantic rule. Use help command-name to review the user-visible arguments and input/output table. scope commands and the help commands table can help when checking how Nushell has registered a definition. In a library module, test those examples in a fresh Nu process so a stale interactive definition cannot hide a syntax or import problem.

Be deliberate about widening a signature. Adding any may silence a useful mismatch, while adding a second pipeline pair changes what downstream code is allowed to assume. Conversely, an output signature that claims a narrow shape should be supported by the command’s actual return paths. Keep the declaration, implementation, tests, and help text in agreement.

This article focuses on command contracts, not worker scheduling or ordering in par-each, environment mutation across blocks, or the lifecycle of modules and overlays. Those are separate boundaries with their own failure modes.

Related:

Sources:

Comments