Value parsers
Value parsers are the specialized components responsible for converting raw string input from command-line arguments into strongly-typed values. While command-line arguments are always strings, your application needs them as numbers, URLs, file paths, or other typed values. Value parsers handle this critical transformation and provide validation at parse time.
The philosophy behind Optique's value parsers is “fail fast, fail clearly.” Rather than letting invalid data flow through your application and cause mysterious errors later, value parsers validate input immediately when parsing occurs. When validation fails, they provide clear, actionable error messages that help users understand what went wrong and how to fix it.
Every value parser implements the ValueParser<T> interface, which defines how to parse strings into values of type T and how to format those values back into strings for help text. This consistent interface makes value parsers composable and allows you to create custom parsers that integrate seamlessly with Optique's type system.
string() parser
The string() parser is the most basic value parser—it accepts any string input and performs optional pattern validation. While all command-line arguments start as strings, the string() parser provides a way to explicitly document that you want string values and optionally validate their format.
import { string } from "@optique/core/valueparser";
// Basic string parser
const name = string();
// String with custom metavar for help text
const hostname = string({ metavar: "HOST" });
// String with pattern validation
const identifier = string({
metavar: "ID",
pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/
});Pattern validation
The optional pattern parameter accepts a regular expression that the input must match:
// Email-like pattern
const email = string({
pattern: /^[^@]+@[^@]+\.[^@]+$/,
metavar: "EMAIL"
});
// Semantic version pattern
const version = string({
pattern: /^\d+\.\d+\.\d+$/,
metavar: "VERSION"
});When pattern validation fails, the parser provides a clear error message indicating what pattern was expected:
$ myapp --version 1.2
Error: Expected a string matching pattern ^\d+\.\d+\.\d+$, but got 1.2.The string() parser uses "STRING" as its default metavar, which appears in help text to indicate what kind of input is expected.
integer() parser
The integer() parser converts string input to numeric values with validation. It supports both regular JavaScript numbers (safe up to Number.MAX_SAFE_INTEGER) and arbitrary-precision bigint values for very large integers.
Number mode (default)
By default, integer() returns JavaScript numbers and validates that input contains only digits:
import { integer } from "@optique/core/valueparser";
// Basic integer
const count = integer();
// Integer with bounds checking
const port = integer({ min: 1, max: 0xffff });
// Integer with custom metavar
const timeout = integer({ min: 0, metavar: "SECONDS" });Bigint mode
For very large integers that exceed JavaScript's safe integer range, specify type: "bigint":
// BigInt integer with bounds
const largeNumber = integer({
type: "bigint",
min: 0n,
max: 999999999999999999999n
});Validation and error messages
The integer() parser provides detailed validation:
- Format validation: Ensures input contains only digits
- Range validation: Enforces minimum and maximum bounds
- Overflow protection: Prevents values outside safe ranges
$ myapp --port "abc"
Error: Expected a valid integer, but got abc.
$ myapp --port "99999"
Error: Expected a value less than or equal to 65,535, but got 99999.The parser uses "INTEGER" as its default metavar.
float() parser
The float() parser handles floating-point numbers with comprehensive validation options. It recognizes standard decimal notation, scientific notation, and optionally special values like NaN and Infinity.
import { float } from "@optique/core/valueparser";
// Basic float
const rate = float();
// Float with bounds
const percentage = float({ min: 0.0, max: 100.0 });
// Float allowing special values
const scientific = float({
allowNaN: true,
allowInfinity: true,
metavar: "NUMBER"
});Supported formats
The float() parser recognizes multiple numeric formats:
- Integers:
42,-17 - Decimals:
3.14,-2.5,.5,42. - Scientific notation:
1.23e10,5.67E-4,1e+5 - Special values:
NaN,Infinity,-Infinity(when allowed)
Special values
By default, NaN and Infinity are not allowed, which prevents accidental acceptance of these values. Enable them explicitly when they're meaningful for your use case:
// Mathematics application that handles infinite limits
const limit = float({
allowInfinity: true,
metavar: "LIMIT"
});
// Statistical application that handles missing data
const value = float({
allowNaN: true,
allowInfinity: true,
metavar: "VALUE"
});The parser uses "NUMBER" as its default metavar and provides clear error messages for invalid formats and out-of-range values.
choice() parser
The choice() parser creates type-safe enumerations by restricting input to one of several predefined string values. This is perfect for options like log levels, output formats, or operation modes where only specific values make sense.
import { choice } from "@optique/core/valueparser";
// Log level choice
const logLevel = choice(["debug", "info", "warn", "error"]);
// Output format choice
const format = choice(["json", "yaml", "xml", "csv"]);
// Case-insensitive choice
const protocol = choice(["http", "https", "ftp"], {
caseInsensitive: true
});Type-safe string literals
The choice() parser creates exact string literal types rather than generic string types, providing excellent TypeScript integration:
const level = choice(["debug", "info", "warn", "error"]);
Case sensitivity
By default, matching is case-sensitive. Set caseInsensitive: true to accept variations:
const format = choice(["JSON", "XML"], {
caseInsensitive: true,
metavar: "FORMAT"
});
// Accepts: "json", "JSON", "Json", "xml", "XML", "Xml"Error messages
When invalid choices are provided, the parser lists all valid options:
$ myapp --format "txt"
Error: Expected one of json, yaml, xml, csv, but got txt.The parser uses "TYPE" as its default metavar.
Number choices
This feature is available since Optique 0.9.0.
The choice() parser also supports number literals, which is useful for options like bit depths, port numbers, or other numeric values where only specific values are valid:
import { choice } from "@optique/core/valueparser";
// Bit depth choice
const bitDepth = choice([8, 10, 12]);
// Port selection
const port = choice([80, 443, 8080]);Number choices provide the same type-safe literal types as string choices:
const depth = choice([8, 10, 12]);
NOTE
The caseInsensitive option is only available for string choices. TypeScript will report an error if you try to use it with number choices.
url() parser
The url() parser validates that input is a well-formed URL and optionally restricts the allowed protocols. The parsed result is a JavaScript URL object with all the benefits of the native URL API.
import { url } from "@optique/core/valueparser";
// Basic URL parser
const endpoint = url();
// URL with protocol restrictions
const apiUrl = url({
allowedProtocols: ["https:"],
metavar: "HTTPS_URL"
});
// URL allowing multiple protocols
const downloadUrl = url({
allowedProtocols: ["http:", "https:", "ftp:"],
metavar: "URL"
});Protocol validation
The allowedProtocols option restricts which URL schemes are acceptable. Protocol names must include the trailing colon:
// Only secure protocols
const secureUrl = url({
allowedProtocols: ["https:", "wss:"]
});
// Web protocols only
const webUrl = url({
allowedProtocols: ["http:", "https:"]
});URL object benefits
The parser returns native URL objects, providing immediate access to URL components:
const result = parse(apiUrl, ["https://api.example.com:8080/v1/users"]);
if (result.success) {
const url = result.value;
console.log(`Host: ${url.hostname}`); // "api.example.com"
console.log(`Port: ${url.port}`); // "8080"
console.log(`Path: ${url.pathname}`); // "/v1/users"
console.log(`Protocol: ${url.protocol}`); // "https:"
}Validation errors
The parser provides specific error messages for different validation failures:
$ myapp --url "not-a-url"
Error: Invalid URL: not-a-url.
$ myapp --url "ftp://example.com" # when only HTTPS allowed
Error: URL protocol ftp: is not allowed. Allowed protocols: https:.The parser uses "URL" as its default metavar.
locale() parser
The locale() parser validates locale identifiers according to the Unicode Locale Identifier standard (BCP 47). It returns Intl.Locale objects that provide rich locale information and integrate with JavaScript's internationalization APIs.
import { locale } from "@optique/core/valueparser";
// Basic locale parser
const userLocale = locale();
// Locale with custom metavar
const displayLocale = locale({ metavar: "LANG" });Locale validation
The parser uses the native Intl.Locale constructor for validation, ensuring compliance with international standards:
// Valid locales
const validLocales = [
"en", // Language only
"en-US", // Language and region
"zh-Hans-CN", // Language, script, and region
"de-DE-u-co-phonebk" // With Unicode extension
];
// Invalid locales will be rejected
const invalidLocales = [
"invalid-locale",
"en_US", // Underscore instead of hyphen
"toolong-language-tag-name"
];Locale object benefits
The parser returns Intl.Locale objects with rich locale information:
const result = parse(userLocale, ["zh-Hans-CN"]);
if (result.success) {
const locale = result.value;
console.log(`Language: ${locale.language}`); // "zh"
console.log(`Script: ${locale.script}`); // "Hans"
console.log(`Region: ${locale.region}`); // "CN"
console.log(`Base name: ${locale.baseName}`); // "zh-Hans-CN"
}The parser uses "LOCALE" as its default metavar and provides clear error messages for invalid locale identifiers.
uuid() parser
The uuid() parser validates UUID (Universally Unique Identifier) strings according to the standard format and optionally restricts to specific UUID versions. It returns a branded Uuid string type for additional type safety.
import { uuid } from "@optique/core/valueparser";
// Basic UUID parser
const id = uuid();
// UUID with version restrictions
const uuidV4 = uuid({
allowedVersions: [4],
metavar: "UUID_V4"
});
// UUID allowing multiple versions
const trackingId = uuid({
allowedVersions: [1, 4, 5]
});UUID format validation
The parser validates the standard UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx where each x is a hexadecimal digit:
# Valid UUID formats
550e8400-e29b-41d4-a716-446655440000 # Version 1
123e4567-e89b-12d3-a456-426614174000 # Version 1
6ba7b810-9dad-11d1-80b4-00c04fd430c8 # Version 1
6ba7b811-9dad-11d1-80b4-00c04fd430c8 # Version 1
# Invalid formats
550e8400-e29b-41d4-a716-44665544000 # Too short
550e8400-e29b-41d4-a716-446655440000x # Extra character
550e8400e29b41d4a716446655440000 # Missing hyphensVersion validation
When allowedVersions is specified, the parser checks the version digit (character 14) and validates it matches one of the allowed versions:
const uuidV4Only = uuid({ allowedVersions: [4] });
// This will pass validation (version 4 UUID)
const validV4 = "550e8400-e29b-41d4-a716-446655440000";
// This will fail validation (version 1 UUID)
const invalidV1 = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";UUID type safety
The parser returns a branded Uuid type rather than a plain string, providing additional compile-time safety:
type Uuid = `${string}-${string}-${string}-${string}-${string}`;
// The branded type prevents accidental usage of regular strings as UUIDs
function processUuid(id: Uuid) {
// Implementation here
}
const result = parse(uuidParser, ["550e8400-e29b-41d4-a716-446655440000"]);
if (result.success) {
processUuid(result.value); // ✓ Type-safe
}
// This would cause a TypeScript error:
processUuid("not-a-uuid"); // ✗ Type errorThe parser uses "UUID" as its default metavar and provides specific error messages for format violations and version mismatches.
port() parser
The port() parser validates TCP/UDP port numbers with support for both number and bigint types, range validation, and well-known port restrictions. Port numbers are commonly used in network applications for server addresses, database connections, and service configurations.
import { port } from "@optique/core/valueparser";
// Basic port parser (1-65535)
const serverPort = port();
// Non-privileged ports only (1024-65535)
const userPort = port({ min: 1024, max: 65535 });
// Development ports
const devPort = port({ min: 3000, max: 9000 });
// Disallow well-known ports (1-1023)
const appPort = port({ disallowWellKnown: true });Port number validation
By default, the port() parser validates that the input is a valid port number between 1 and 65535 (the full range of valid TCP/UDP ports). You can customize this range using the min and max options:
// Web server ports (typically 80 or 443)
const webPort = port({ min: 1, max: 65535 });
// Custom application ports
const customPort = port({ min: 8000, max: 8999 });Well-known port restrictions
The disallowWellKnown option rejects ports 1–1023, which typically require elevated privileges (root/administrator) to bind on most systems. This is useful for applications that should run without special permissions:
const unprivilegedPort = port({
disallowWellKnown: true,
metavar: "PORT"
});When a well-known port is rejected, the error message explains why:
$ myapp --port 80
Error: Port 80 is a well-known port (1-1023) and may require elevated privileges.Number and bigint modes
Like the integer() parser, port() supports both number (default) and bigint types. While all valid port numbers fit safely in JavaScript's number type, the bigint option is provided for consistency with other numeric parsers:
// Default: returns number
const numPort = port();
// Bigint mode: returns bigint
const bigintPort = port({ type: "bigint" });Common use cases
The port() parser is commonly used in network applications:
// HTTP server
const httpPort = option("--port", port({ min: 1024 }));
// Database connection
const dbPort = option("--db-port", port());
// Redis connection (default port 6379)
const redisPort = option("--redis-port", port());The parser uses "PORT" as its default metavar and provides specific error messages for invalid port numbers, range violations, and well-known port restrictions.
ipv4() parser
The ipv4() parser validates IPv4 addresses in dotted-decimal notation with comprehensive filtering options for different IP address types. It's commonly used for network configuration, server addresses, and IP allowlists/blocklists.
import { ipv4 } from "@optique/core/valueparser";
// Basic IPv4 parser (allows all types)
const address = ipv4();
// Public IPs only (no private/loopback)
const publicIp = ipv4({
allowPrivate: false,
allowLoopback: false
});
// Server binding (allow 0.0.0.0 and private IPs)
const bindAddress = ipv4({
allowZero: true,
allowPrivate: true
});IPv4 format validation
The ipv4() parser validates that input matches the standard IPv4 format: four decimal octets (0-255) separated by dots (e.g., “192.168.1.1”). Each octet must be in the valid range, and the parser strictly rejects:
- Leading zeros: “192.168.001.1” is invalid (except single “0”)
- Whitespace: Leading, trailing, or embedded spaces are rejected
- Empty octets: “192.168..1” is invalid
- Out-of-range values: Octets must be 0-255
const parser = ipv4();
// Valid IPv4 addresses
parser.parse("192.168.1.1"); // ✓
parser.parse("10.0.0.1"); // ✓
parser.parse("255.255.255.255"); // ✓
parser.parse("0.0.0.0"); // ✓
// Invalid formats
parser.parse("192.168.001.1"); // ✗ Leading zero
parser.parse("256.1.1.1"); // ✗ Octet > 255
parser.parse("192.168.1"); // ✗ Only 3 octets
parser.parse("192.168..1"); // ✗ Empty octetIP address filtering
The parser provides fine-grained control over which IP address types are accepted through boolean filter options. All filters default to true (permissive), allowing you to selectively restrict specific address types:
allowPrivate- Controls private IP ranges (RFC 1918): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Set to
falseto reject these addresses. allowLoopback- Controls loopback addresses (127.0.0.0/8). Set to
falseto reject addresses like 127.0.0.1. allowLinkLocal- Controls link-local addresses (169.254.0.0/16). Set to
falseto reject APIPA/link-local addresses. allowMulticast- Controls multicast addresses (224.0.0.0/4). Set to
falseto reject addresses in the 224-239 range. allowBroadcast- Controls the broadcast address (255.255.255.255). Set to
falseto reject the all-hosts broadcast address. allowZero- Controls the zero address (0.0.0.0). Set to
falseto reject the “any” or “unspecified” address.
// Public-facing API endpoint (no private/loopback IPs)
const publicEndpoint = option("--api-endpoint", ipv4({
allowPrivate: false,
allowLoopback: false,
allowLinkLocal: false
}));
// Server binding address (allow 0.0.0.0 and private IPs)
const bindAddr = option("--bind", ipv4({
allowZero: true,
allowPrivate: true
}));
// Client IP address (no special addresses)
const clientIp = option("--client-ip", ipv4({
allowZero: false,
allowBroadcast: false,
allowMulticast: false
}));Error messages
The parser provides specific error messages for different validation failures:
$ myapp --ip "192.168.001.1"
Error: Expected a valid IPv4 address, but got 192.168.001.1.
$ myapp --public-ip "192.168.1.1" # when private IPs are disallowed
Error: 192.168.1.1 is a private IP address.
$ myapp --endpoint "127.0.0.1" # when loopback is disallowed
Error: 127.0.0.1 is a loopback address.
$ myapp --bind "255.255.255.255" # when broadcast is disallowed
Error: 255.255.255.255 is the broadcast address.Common use cases
The ipv4() parser is commonly used in network applications:
// HTTP server configuration
const serverConfig = object({
bind: option("--bind", ipv4({ allowPrivate: true })),
port: option("--port", port({ min: 1024 }))
});
// Firewall rule configuration
const firewallRule = object({
source: option("--source", ipv4()),
dest: option("--dest", ipv4())
});
// DNS server configuration
const dnsConfig = option("--nameserver", ipv4({
allowLoopback: false, // Loopback doesn't make sense for DNS
allowZero: false // 0.0.0.0 not valid for nameserver
}));The parser uses "IPV4" as its default metavar and returns the normalized IPv4 address as a string (e.g., “192.168.1.1”).
hostname() parser
The hostname() parser validates DNS hostnames according to RFC 1123. It supports flexible options for wildcard hostnames, underscores, localhost filtering, and length constraints.
import { hostname } from "@optique/core/valueparser";
// Basic hostname parser
const host = hostname();
// Allow wildcards for certificate validation
const domain = hostname({ allowWildcard: true });
// Reject localhost
const remoteHost = hostname({ allowLocalhost: false });
// Service discovery records (with underscores)
const srvRecord = hostname({ allowUnderscore: true });Hostname validation rules
The parser validates hostnames according to RFC 1123:
- Labels are separated by dots (
.) - Each label must be 1-63 characters long
- Labels can contain alphanumeric characters and hyphens (
-) - Labels cannot start or end with a hyphen
- Total hostname length must not exceed 253 characters (by default)
For example:
- Valid:
"example.com","sub.example.com","server-01.local" - Invalid:
"-example.com"(starts with hyphen),"example..com"(empty label),"a".repeat(64) + ".com"(label too long)
Wildcard hostnames
By default, wildcard hostnames (starting with *.) are rejected. Enable them with the allowWildcard option:
// Allow wildcards for SSL certificate domains
const certDomain = option("--domain", hostname({ allowWildcard: true }));$ example --domain "*.example.com" # Valid
$ example --domain "*.*.example.com" # Invalid: multiple wildcards not allowedUnderscores in hostnames
While technically invalid per RFC 1123, underscores are commonly used in some contexts like service discovery records. Enable them with allowUnderscore:
// Allow underscores for service discovery
const service = option("--srv", hostname({ allowUnderscore: true }));$ example --srv "_http._tcp.example.com" # Valid with allowUnderscore
$ example --srv "_http._tcp.example.com" # Invalid by defaultLocalhost filtering
The parser accepts "localhost" by default. Reject it with allowLocalhost: false:
// Require remote hosts only
const remoteHost = option("--remote", hostname({ allowLocalhost: false }));$ example --remote "localhost" # Error: Hostname 'localhost' is not allowed.
$ example --remote "example.com" # ValidLength constraints
The default maximum hostname length is 253 characters (the RFC 1123 limit). Customize it with the maxLength option:
// Limit hostname length to 50 characters
const shortHost = option("--host", hostname({ maxLength: 50 }));Custom error messages
Customize error messages for various validation failures:
const host = option("--host", hostname({
allowWildcard: false,
allowLocalhost: false,
errors: {
invalidHostname: (input) => message`Not a valid hostname: ${input}`,
wildcardNotAllowed: message`Wildcards are forbidden`,
localhostNotAllowed: message`Remote hosts only`,
tooLong: (hostname, max) =>
message`Hostname too long (max ${text(max.toString())} chars)`,
},
}));Common use cases
Web server host configuration:
// Allow localhost for local development
const serverHost = hostname({ allowLocalhost: true });Remote database connections:
// Require remote hosts only
const dbHost = hostname({ allowLocalhost: false });SSL certificate domain validation:
// Allow wildcards for certificate domains
const certDomain = hostname({ allowWildcard: true });The parser uses "HOST" as its default metavar and returns the hostname as-is, preserving the original case.
email() parser
The email() parser validates email addresses according to simplified RFC 5322 addr-spec format. It supports display names, multiple addresses, domain filtering, and quoted local parts for practical email validation use cases.
import { email } from "@optique/core/valueparser";
// Basic email parser
const userEmail = email();
// Multiple comma-separated addresses
const recipients = email({ allowMultiple: true });
// Allow display names
const from = email({ allowDisplayName: true });
// Restrict to specific domains
const workEmail = email({ allowedDomains: ["company.com"] });Email validation rules
The parser validates email addresses using simplified RFC 5322 rules:
- Format:
local-part@domain - Local part: alphanumeric characters, dots (
.), hyphens (-), underscores (_), and plus signs (+) - Domain part: valid hostname with at least one dot
- Quoted local parts (e.g.,
"user name"@example.com) allow spaces and special characters
For example:
- Valid:
"user@example.com","first.last@example.com","user+tag@mail.example.com" - Invalid:
"userexample.com"(no @ sign),"user@example"(no dot in domain),"user..name@example.com"(consecutive dots)
Display name support
By default, display names are rejected. Enable them with the allowDisplayName option:
// Allow "Name <email@example.com>" format
const from = option("--from", email({ allowDisplayName: true }));$ example --from "John Doe <john@example.com>" # Valid
$ example --from "john@example.com" # Also validThe parser extracts the email address and discards the display name. Both formats ("Name <email>" and Name <email>) are accepted.
Multiple email addresses
Parse comma-separated email lists with allowMultiple:
// Accept multiple comma-separated emails
const to = option("--to", email({ allowMultiple: true }));$ example --to "alice@example.com,bob@example.com" # Valid
$ example --to "alice@example.com, bob@example.com" # Whitespace trimmedWhen allowMultiple is true, the parser returns a readonly string[] instead of a single string.
Domain filtering
Restrict accepted email addresses to specific domains with allowedDomains:
// Only accept company email addresses
const workEmail = option(
"--email",
email({ allowedDomains: ["company.com", "company.org"] })
);$ example --email "user@company.com" # Valid
$ example --email "user@gmail.com" # Error: domain not allowedDomain matching is case-insensitive.
Lowercase conversion
Convert email addresses to lowercase with the lowercase option:
// Normalize email to lowercase
const normalizedEmail = option("--email", email({ lowercase: true }));$ example --email "User@Example.COM" # Returns: "user@example.com"Quoted local parts
The parser supports quoted strings in local parts for special characters:
const parser = email();
const result = parser.parse('"user name"@example.com');
// result.value = '"user name"@example.com'Quoted strings allow spaces and special characters that are normally forbidden in local parts, such as @ signs inside the quotes.
Custom error messages
Customize error messages for validation failures:
const workEmail = option("--email", email({
allowedDomains: ["company.com"],
errors: {
invalidEmail: (input) => message`Invalid email format: ${input}`,
domainNotAllowed: (email, domains) =>
message`Email ${email} must use company domain`,
},
}));Common use cases
User registration email:
// Normalize email for consistent storage
const userEmail = email({ lowercase: true });Notification recipients:
// Multiple recipients with display names
const recipients = email({ allowMultiple: true, allowDisplayName: true });Corporate email restriction:
// Only accept company domain emails
const corporateEmail = email({
allowedDomains: ["company.com", "company.org"],
lowercase: true,
});The parser uses "EMAIL" as its default metavar.
socketAddress() parser
The socketAddress() parser validates socket addresses in “host:port” format. It supports both hostnames and IPv4 addresses, configurable separators, default ports, and comprehensive host/port validation options. The parser returns a SocketAddressValue object containing both the host and port components.
import { socketAddress } from "@optique/core/valueparser";
// Basic socket address (requires port)
const endpoint = socketAddress({ requirePort: true });
// With default port
const server = socketAddress({ defaultPort: 80 });
// IP addresses only
const bind = socketAddress({
defaultPort: 8080,
host: { type: "ip" },
});
// Non-privileged ports only
const listen = socketAddress({
defaultPort: 8080,
port: { min: 1024 },
});Socket address format
The parser accepts addresses in the following format:
- With port:
host:port(e.g.,"localhost:3000","192.168.1.1:80") - Without port:
host(only whendefaultPortis set, e.g.,"example.com")
The separator between host and port can be customized using the separator option.
Port requirements
By default, the port is optional if defaultPort is specified. Control this behavior with the requirePort option:
// Port always required
const endpoint = option(
"--endpoint",
socketAddress({ requirePort: true })
);$ example --endpoint "localhost:3000" # Valid
$ example --endpoint "localhost" # Error: port requiredWhen requirePort is false (default) and defaultPort is set, the port may be omitted:
// Port optional, defaults to 80
const server = option(
"--server",
socketAddress({ defaultPort: 80 })
);$ example --server "example.com:443" # Uses port 443
$ example --server "example.com" # Uses default port 80Host type filtering
The host.type option controls what types of hosts are accepted:
"hostname": Accept only valid hostnames"ip": Accept only IP addresses (currently IPv4 only)"both": Accept both hostnames and IP addresses (default)
// Only accept IP addresses for binding
const bind = option(
"--bind",
socketAddress({
defaultPort: 8080,
host: { type: "ip" },
})
);$ example --bind "0.0.0.0:8080" # Valid
$ example --bind "localhost:8080" # Error: hostname not allowedHost validation options
Pass options to the underlying hostname or IP parser using host.hostname or host.ip:
// Remote hosts only (no localhost)
const remote = option(
"--remote",
socketAddress({
defaultPort: 80,
host: {
type: "hostname",
hostname: { allowLocalhost: false },
},
})
);$ example --remote "localhost:80" # Error: localhost not allowed
$ example --remote "example.com:80" # ValidFor IP addresses, use host.ip to pass options like allowPrivate:
// Public IPs only
const publicServer = option(
"--server",
socketAddress({
defaultPort: 443,
host: {
type: "ip",
ip: { allowPrivate: false },
},
})
);Port validation options
Pass port validation options using the port field:
// Non-privileged ports only
const listen = option(
"--listen",
socketAddress({
defaultPort: 8080,
port: { min: 1024, max: 65535 },
})
);$ example --listen "localhost:80" # Error: port too low
$ example --listen "localhost:8080" # ValidDisallow well-known ports (1-1023):
const server = option(
"--server",
socketAddress({
defaultPort: 8080,
port: { disallowWellKnown: true },
})
);Custom separator
Change the separator between host and port:
// Use space as separator
const proxy = option(
"--proxy",
socketAddress({ separator: " ", defaultPort: 8080 })
);$ example --proxy "localhost 3000" # Valid
$ example --proxy "localhost:3000" # Invalid: wrong separatorReturn value
The parser returns a SocketAddressValue object:
const parser = socketAddress({ defaultPort: 80 });
const result = parser.parse("example.com:443");
if (result.success) {
console.log(result.value.host); // "example.com"
console.log(result.value.port); // 443
}Custom error messages
Customize error messages for validation failures:
const endpoint = option(
"--endpoint",
socketAddress({
requirePort: true,
errors: {
invalidFormat: (input) => message`Invalid endpoint: ${input}`,
missingPort: message`You must specify a port number`,
},
})
);Common use cases
Web server configuration:
// Allow any host, default to port 8080
const listen = socketAddress({ defaultPort: 8080 });Database connections:
// Require explicit host and port
const dbServer = socketAddress({ requirePort: true });Proxy configuration:
// Accept hostnames and IPs, default port 3128
const proxy = socketAddress({
defaultPort: 3128,
host: { type: "both" },
});Service binding (IP addresses only):
// Bind to IP addresses only, non-privileged ports
const bind = socketAddress({
defaultPort: 8080,
host: { type: "ip" },
port: { min: 1024 },
});The parser uses "HOST:PORT" as its default metavar.
portRange() parser
The portRange() parser validates port ranges in the format start-end (e.g., 8000-8080). It supports both number and bigint types, custom separators, and comprehensive validation options including min/max constraints and well-known port filtering.
import { portRange } from "@optique/core/valueparser";
// Basic port range (number type)
const range = portRange();
// Custom separator
const colonRange = portRange({ separator: ":" });
// Allow single ports (returns {start: 8080, end: 8080})
const flexibleRange = portRange({ allowSingle: true });
// Restrict to non-privileged ports
const unprivilegedRange = portRange({ min: 1024 });
// BigInt type for consistency with other APIs
const bigintRange = portRange({ type: "bigint" });Port range format
The parser accepts port ranges in start-end format where both start and end must be valid port numbers (1-65535):
parser.parse("8000-8080"); // { start: 8000, end: 8080 }
parser.parse("80-443"); // { start: 80, end: 443 }
parser.parse("1-65535"); // { start: 1, end: 65535 }The start port must be less than or equal to the end port:
parser.parse("8080-8000"); // Error: start port must not be greater than end portSingle port mode
When allowSingle is enabled, the parser accepts single port numbers and returns a range where start equals end:
import { portRange } from "@optique/core/valueparser";
const parser = portRange({ allowSingle: true });
parser.parse("8080"); // { start: 8080, end: 8080 }
parser.parse("80-443"); // { start: 80, end: 443 }Without allowSingle, single ports are rejected:
import { portRange } from "@optique/core/valueparser";
const parser = portRange();
parser.parse("8080"); // Error: must be in the format PORT-PORTCustom separator
The default separator is "-", but you can specify any string:
import { portRange } from "@optique/core/valueparser";
// Colon separator
const colonRange = portRange({ separator: ":" });
colonRange.parse("8000:8080"); // { start: 8000, end: 8080 }
// Multi-character separator
const toRange = portRange({ separator: " to " });
toRange.parse("8000 to 8080"); // { start: 8000, end: 8080 }The separator is also used in the metavar and error messages:
import { portRange } from "@optique/core/valueparser";
portRange({ separator: ":" }).metavar; // "PORT:PORT"
portRange({ separator: " to " }).metavar; // "PORT to PORT"Min/max constraints
Restrict the allowed port range using min and max options. These constraints apply to both the start and end ports:
import { portRange } from "@optique/core/valueparser";
// Only non-privileged ports
const unprivileged = portRange({ min: 1024 });
unprivileged.parse("1024-8080"); // { start: 1024, end: 8080 }
unprivileged.parse("80-443"); // Error: must be at least 1024
// Limit to ephemeral port range
const ephemeral = portRange({ min: 49152, max: 65535 });
ephemeral.parse("50000-51000"); // { start: 50000, end: 51000 }
ephemeral.parse("8000-8080"); // Error: must be at least 49152Well-known port filtering
The disallowWellKnown option rejects well-known ports (1-1023) in both the start and end positions:
import { portRange } from "@optique/core/valueparser";
const parser = portRange({ disallowWellKnown: true });
parser.parse("1024-8080"); // { start: 1024, end: 8080 }
parser.parse("80-443"); // Error: must not be a well-known port (1-1023)
parser.parse("1024-1023"); // Error: must not be a well-known port (1-1023)Number vs bigint types
The parser supports both number (default) and bigint types:
import { portRange } from "@optique/core/valueparser";
// Number type (default)
const numRange = portRange();
numRange.parse("8000-8080"); // { start: 8000, end: 8080 }
// BigInt type
const bigintRange = portRange({ type: "bigint" });
bigintRange.parse("8000-8080"); // { start: 8000n, end: 8080n }The bigint type is useful for consistency when working with APIs that use bigint for port numbers.
Return value
The parser returns a PortRangeValue object with start and end properties:
import { portRange } from "@optique/core/valueparser";
interface PortRangeValueNumber {
readonly start: number;
readonly end: number;
}
interface PortRangeValueBigInt {
readonly start: bigint;
readonly end: bigint;
}The type is inferred based on the type option:
import { portRange } from "@optique/core/valueparser";
const numRange = portRange();
const result1 = numRange.parse("8000-8080");
if (result1.success) {
result1.value.start; // type: number
}
const bigintRange = portRange({ type: "bigint" });
const result2 = bigintRange.parse("8000-8080");
if (result2.success) {
result2.value.start; // type: bigint
}Custom error messages
All error messages can be customized via the errors option:
const portOpt = option("--ports", portRange({
errors: {
invalidFormat: message`Port range must be START-END`,
invalidRange: message`START must be ≤ END`,
invalidPort: message`Ports must be 1-65535`,
belowMinimum: (port, min) =>
message`Port ${text(port.toString())} is below minimum ${text(min.toString())}`,
aboveMaximum: (port, max) =>
message`Port ${text(port.toString())} is above maximum ${text(max.toString())}`,
wellKnownNotAllowed: message`System ports (1-1023) are not allowed`,
},
}));The invalidFormat and invalidRange errors are specific to port ranges, while other errors (invalidPort, belowMinimum, aboveMaximum, wellKnownNotAllowed) are inherited from the underlying port() parser.
Common use cases
Dynamic server port pools:
// Accept ranges or single ports for service binding
const ports = portRange({
allowSingle: true,
min: 1024,
metavar: "PORTS",
});Load balancer configuration:
// Backend server port range
const backendPorts = portRange({
min: 8000,
max: 9000,
});Firewall rules:
// Allow specifying port ranges for firewall configuration
const allowedPorts = portRange({
allowSingle: true,
disallowWellKnown: true,
});Container port mapping:
// Map container port ranges (colon separator for Docker-style syntax)
const portMapping = portRange({
separator: ":",
min: 1024,
});The parser uses "PORT-PORT" as its default metavar (or "PORT{separator}PORT" when a custom separator is specified).
macAddress() parser
The macAddress() parser validates MAC (Media Access Control) addresses, commonly used to identify network interface hardware. It accepts MAC-48 addresses (6 octets, 12 hexadecimal digits) in multiple common formats with support for case conversion and output normalization.
Supported formats
The parser accepts MAC addresses in four standard formats:
import { macAddress } from "@optique/core/valueparser";
const parser = macAddress();
const result1 = parser.parse("00:1A:2B:3C:4D:5E"); // Colon-separated
const result2 = parser.parse("00-1A-2B-3C-4D-5E"); // Hyphen-separated
const result3 = parser.parse("001A.2B3C.4D5E"); // Cisco format (dot-separated)
const result4 = parser.parse("001A2B3C4D5E"); // No separatorBy default, the parser accepts any of these formats. You can restrict it to a specific format using the separator option:
// Accept only colon-separated format
const colonOnly = macAddress({ separator: ":" });
const result = colonOnly.parse("00:1A:2B:3C:4D:5E");
if (result.success) {
result.value; // "00:1A:2B:3C:4D:5E"
}
// Rejects other formats
const invalid = colonOnly.parse("00-1A-2B-3C-4D-5E");
if (!invalid.success) {
invalid.error; // "Invalid MAC address."
}The separator option accepts:
":"- Colon-separated format (e.g.,00:1A:2B:3C:4D:5E)"-"- Hyphen-separated format (e.g.,00-1A-2B-3C-4D-5E)"."- Cisco dot notation (e.g.,001A.2B3C.4D5E)"none"- No separator (e.g.,001A2B3C4D5E)"any"- Accept any format (default)
Case conversion
The parser provides case conversion options for hexadecimal digits:
// Preserve original case (default)
const preserveCase = macAddress({ case: "preserve" });
const result1 = preserveCase.parse("00:1a:2B:3c:4D:5e");
if (result1.success) {
result1.value; // "00:1a:2B:3c:4D:5e"
}
// Convert to uppercase
const upperCase = macAddress({ case: "upper" });
const result2 = upperCase.parse("00:1a:2b:3c:4d:5e");
if (result2.success) {
result2.value; // "00:1A:2B:3C:4D:5E"
}
// Convert to lowercase
const lowerCase = macAddress({ case: "lower" });
const result3 = lowerCase.parse("00:1A:2B:3C:4D:5E");
if (result3.success) {
result3.value; // "00:1a:2b:3c:4d:5e"
}Output normalization
The outputSeparator option normalizes the output format regardless of the input format:
// Normalize all inputs to colon-separated
const normalize = macAddress({
outputSeparator: ":",
case: "upper",
});
const result1 = normalize.parse("00-1a-2b-3c-4d-5e");
if (result1.success) {
result1.value; // "00:1A:2B:3C:4D:5E"
}
const result2 = normalize.parse("001a.2b3c.4d5e");
if (result2.success) {
result2.value; // "00:1A:2B:3C:4D:5E"
}The outputSeparator option accepts the same values as separator except "any". When not specified, the output preserves the input format.
Return value
The parser returns a formatted string according to the case and outputSeparator options:
const parser = macAddress({
outputSeparator: ":",
case: "upper",
});
const result = parser.parse("00-1a-2b-3c-4d-5e");
if (result.success) {
const mac: string = result.value; // "00:1A:2B:3C:4D:5E"
}Custom error messages
You can customize error messages using the errors option:
const parser = macAddress({
errors: {
invalidMacAddress: (input) =>
message`Invalid MAC address: ${text(input)}. Expected format: XX:XX:XX:XX:XX:XX`,
},
});
const result = parser.parse("not-a-mac");
if (!result.success) {
result.error; // "Invalid MAC address: not-a-mac. Expected format: XX:XX:XX:XX:XX:XX"
}Common use cases
Network device configuration:
// Standardize MAC addresses to uppercase colon format
const deviceMac = macAddress({
outputSeparator: ":",
case: "upper",
});Cisco router configuration:
// Accept only Cisco dot notation format
const ciscoMac = macAddress({
separator: ".",
case: "lower",
});Access control lists:
// Accept any format but normalize for storage
const aclMac = macAddress({
outputSeparator: "none",
case: "upper",
});Network monitoring tools:
// Accept any format for user convenience
const monitorMac = macAddress();The parser uses "MAC" as its default metavar.
domain() parser
The domain() parser validates domain names according to RFC 1035 with configurable options for subdomain filtering, TLD restrictions, minimum label requirements, and case normalization.
Basic validation
The parser validates domain names with the following rules:
- Each label (part separated by dots) must be 1-63 characters
- Labels can contain alphanumeric characters and hyphens
- Labels cannot start or end with a hyphen
- By default, requires at least 2 labels (e.g.,
example.com)
import { domain } from "@optique/core/valueparser";
const parser = domain();
const result1 = parser.parse("example.com");
if (result1.success) {
result1.value; // "example.com"
}
const result2 = parser.parse("www.example.com");
if (result2.success) {
result2.value; // "www.example.com"
}
const result3 = parser.parse("api.staging.example.com");
if (result3.success) {
result3.value; // "api.staging.example.com"
}Subdomain filtering
Use the allowSubdomains option to restrict to root domains only:
// Accept only root domains (2 labels)
const rootOnly = domain({ allowSubdomains: false });
const result1 = rootOnly.parse("example.com");
if (result1.success) {
result1.value; // "example.com"
}
// Rejects subdomains
const result2 = rootOnly.parse("www.example.com");
if (!result2.success) {
result2.error; // "Subdomains are not allowed, but got www.example.com."
}TLD restrictions
Use the allowedTLDs option to restrict accepted top-level domains:
// Accept only specific TLDs
const restrictedTLD = domain({ allowedTLDs: ["com", "org", "net"] });
const result1 = restrictedTLD.parse("example.com");
if (result1.success) {
result1.value; // "example.com"
}
// Case-insensitive TLD matching
const result2 = restrictedTLD.parse("example.COM");
if (result2.success) {
result2.value; // "example.COM"
}
// Rejects disallowed TLDs
const result3 = restrictedTLD.parse("example.io");
if (!result3.success) {
result3.error; // "Top-level domain io is not allowed. Allowed TLDs: com, org, net."
}Minimum labels
Use the minLabels option to require a specific number of labels:
// Require at least 3 labels
const threeLabels = domain({ minLabels: 3 });
const result1 = threeLabels.parse("www.example.com");
if (result1.success) {
result1.value; // "www.example.com"
}
const result2 = threeLabels.parse("example.com");
if (!result2.success) {
result2.error; // "Domain example.com must have at least 3 labels."
}
// Allow single-label domains (e.g., "localhost")
const singleLabel = domain({ minLabels: 1 });
const result3 = singleLabel.parse("localhost");
if (result3.success) {
result3.value; // "localhost"
}Case normalization
Use the lowercase option to normalize domain names to lowercase:
// Preserve original case (default)
const preserveCase = domain();
const result1 = preserveCase.parse("Example.COM");
if (result1.success) {
result1.value; // "Example.COM"
}
// Convert to lowercase
const lowerCase = domain({ lowercase: true });
const result2 = lowerCase.parse("Example.COM");
if (result2.success) {
result2.value; // "example.com"
}Return value
The parser returns a string representing the domain name, optionally normalized to lowercase:
const parser = domain({ lowercase: true });
const result = parser.parse("WWW.Example.COM");
if (result.success) {
const domainName: string = result.value; // "www.example.com"
}Custom error messages
You can customize error messages using the errors option:
const parser = domain({
allowSubdomains: false,
allowedTLDs: ["com", "org"],
errors: {
invalidDomain: (input) =>
message`Invalid domain: ${text(input)}`,
subdomainsNotAllowed: (domain) =>
message`Only root domains allowed, got: ${text(domain)}`,
tldNotAllowed: (tld, allowed) =>
message`TLD ${text(tld)} not in: ${text(allowed.join(", "))}`,
tooFewLabels: (domain, min) =>
message`${text(domain)} needs ${text(min.toString())} labels`,
},
});
const result = parser.parse("example..com");
if (!result.success) {
result.error; // Custom error message
}Common use cases
Website configuration:
// Accept any valid domain, normalize to lowercase
const websiteDomain = domain({ lowercase: true });Email domain validation:
// Restrict to specific TLDs for corporate email
const emailDomain = domain({
allowedTLDs: ["com", "org", "edu"],
lowercase: true,
});DNS configuration:
// Root domains only for DNS zone setup
const zoneDomain = domain({
allowSubdomains: false,
lowercase: true,
});API endpoint configuration:
// Any valid domain with minimum 2 labels
const apiDomain = domain({
minLabels: 2,
lowercase: true,
});The parser uses "DOMAIN" as its default metavar.
ipv6() parser
The ipv6() parser validates and normalizes IPv6 addresses to canonical form (lowercase, compressed using :: notation where appropriate). It supports full addresses, compressed notation, and IPv4-mapped IPv6 addresses, with configurable address type restrictions.
Basic validation
The parser validates IPv6 addresses in various formats:
import { ipv6 } from "@optique/core/valueparser";
const parser = ipv6();
// Full format
const result1 = parser.parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
if (result1.success) {
result1.value; // "2001:db8:85a3::8a2e:370:7334" (compressed)
}
// Compressed format
const result2 = parser.parse("2001:db8::1");
if (result2.success) {
result2.value; // "2001:db8::1"
}
// IPv4-mapped IPv6
const result3 = parser.parse("::ffff:192.0.2.1");
if (result3.success) {
result3.value; // "::ffff:c000:201"
}
// Loopback
const result4 = parser.parse("::1");
if (result4.success) {
result4.value; // "::1"
}Address type filtering
The parser provides options to restrict specific address types:
// No loopback addresses
const noLoopback = ipv6({ allowLoopback: false });
const result1 = noLoopback.parse("::1");
if (!result1.success) {
result1.error; // "::1 is a loopback address."
}
// Global unicast only (no link-local, no unique local)
const publicOnly = ipv6({
allowLinkLocal: false,
allowUniqueLocal: false,
});
const result2 = publicOnly.parse("fe80::1");
if (!result2.success) {
result2.error; // "fe80::1 is a link-local address."
}
const result3 = publicOnly.parse("fc00::1");
if (!result3.success) {
result3.error; // "fc00::1 is a unique local address."
}Address type categories
The parser recognizes these IPv6 address types:
- Loopback (
::1): Local loopback address - Link-local (
fe80::/10): Addresses for single network segment - Unique local (
fc00::/7): Private addresses for local communication - Multicast (
ff00::/8): Addresses for multicast groups - Zero (
::): All-zeros address
Normalization
The parser automatically normalizes IPv6 addresses to canonical form:
- Converts to lowercase
- Removes leading zeros from each group
- Compresses the longest sequence of consecutive zero groups with
::
const parser = ipv6();
// Uppercase converted to lowercase
const result1 = parser.parse("2001:DB8:85A3::8A2E:370:7334");
if (result1.success) {
result1.value; // "2001:db8:85a3::8a2e:370:7334"
}
// Leading zeros removed and compressed
const result2 = parser.parse("2001:0db8:0000:0000:0000:0000:0000:0001");
if (result2.success) {
result2.value; // "2001:db8::1"
}Return value
The parser returns a string representing the normalized IPv6 address in canonical form (lowercase, compressed).
The parser uses "IPV6" as its default metavar.
ip() parser
The ip() parser accepts both IPv4 and IPv6 addresses, delegating to the ipv4() and ipv6() parsers based on the detected format. It can be configured to accept only IPv4, only IPv6, or both (default).
Basic validation
By default, the parser accepts both IP versions:
import { ip } from "@optique/core/valueparser";
const parser = ip();
// IPv4 address
const result1 = parser.parse("192.0.2.1");
if (result1.success) {
result1.value; // "192.0.2.1"
}
// IPv6 address
const result2 = parser.parse("2001:db8::1");
if (result2.success) {
result2.value; // "2001:db8::1"
}Version filtering
Use the version option to restrict to a specific IP version:
// IPv4 only
const ipv4Only = ip({ version: 4 });
const result1 = ipv4Only.parse("192.0.2.1");
if (result1.success) {
result1.value; // "192.0.2.1"
}
const result2 = ipv4Only.parse("2001:db8::1");
if (!result2.success) {
result2.error; // "Expected a valid IP address, but got 2001:db8::1."
}
// IPv6 only
const ipv6Only = ip({ version: 6 });
const result3 = ipv6Only.parse("2001:db8::1");
if (result3.success) {
result3.value; // "2001:db8::1"
}Passing options to IPv4/IPv6 parsers
The parser accepts separate options for IPv4 and IPv6 validation:
// Public IPs only (both versions)
const publicOnly = ip({
ipv4: { allowPrivate: false, allowLoopback: false },
ipv6: { allowLinkLocal: false, allowUniqueLocal: false },
});
const result1 = publicOnly.parse("192.168.1.1");
if (!result1.success) {
result1.error; // "192.168.1.1 is a private IP address."
}
const result2 = publicOnly.parse("fe80::1");
if (!result2.success) {
result2.error; // "fe80::1 is a link-local address."
}Shared error messages
The parser supports shared error messages that apply to both IP versions:
const parser = ip({
errors: {
loopbackNotAllowed: [
{ type: "text", text: "Loopback addresses are not allowed" },
] satisfies Message,
},
ipv4: { allowLoopback: false },
ipv6: { allowLoopback: false },
});
const result1 = parser.parse("127.0.0.1");
if (!result1.success) {
result1.error; // "Loopback addresses are not allowed"
}
const result2 = parser.parse("::1");
if (!result2.success) {
result2.error; // "Loopback addresses are not allowed"
}Return value
The parser returns a normalized IP address string. IPv4 addresses are returned as-is, while IPv6 addresses are normalized to canonical form (lowercase, compressed).
When version is "both", the parser tries IPv4 first, then IPv6 if IPv4 fails. This means IPv4-mapped IPv6 addresses like ::ffff:192.0.2.1 are parsed as IPv6.
The parser uses "IP" as its default metavar.
cidr() parser
The cidr() parser validates CIDR notation (IP address with prefix length) and returns a structured object containing the normalized IP address, prefix length, and IP version.
Basic validation
The parser validates CIDR notation for both IPv4 and IPv6:
import { cidr } from "@optique/core/valueparser";
const parser = cidr();
// IPv4 CIDR
const result1 = parser.parse("192.0.2.0/24");
if (result1.success) {
result1.value.address; // "192.0.2.0"
result1.value.prefix; // 24
result1.value.version; // 4
}
// IPv6 CIDR
const result2 = parser.parse("2001:db8::/32");
if (result2.success) {
result2.value.address; // "2001:db8::"
result2.value.prefix; // 32
result2.value.version; // 6
}Prefix validation
The parser validates prefix lengths based on IP version:
- IPv4: 0-32
- IPv6: 0-128
const parser = cidr();
// Valid IPv4 prefix
const result1 = parser.parse("192.0.2.0/32");
if (result1.success) {
result1.value.prefix; // 32
}
// Invalid IPv4 prefix (>32)
const result2 = parser.parse("192.0.2.0/33");
if (!result2.success) {
result2.error; // "Expected a prefix length between 0 and 32 for IPv4, but got 33."
}
// Valid IPv6 prefix
const result3 = parser.parse("2001:db8::/128");
if (result3.success) {
result3.value.prefix; // 128
}Prefix constraints
Use minPrefix and maxPrefix options to constrain the prefix length:
// Subnet sizes between /16 and /24
const subnet = cidr({
version: 4,
minPrefix: 16,
maxPrefix: 24,
});
const result1 = subnet.parse("192.0.2.0/20");
if (result1.success) {
result1.value.prefix; // 20
}
const result2 = subnet.parse("192.0.2.0/8");
if (!result2.success) {
result2.error; // "Expected a prefix length greater than or equal to 16, but got 8."
}
const result3 = subnet.parse("192.0.2.0/28");
if (!result3.success) {
result3.error; // "Expected a prefix length less than or equal to 24, but got 28."
}Version filtering
Use the version option to restrict to IPv4 or IPv6 CIDR:
// IPv4 CIDR only
const ipv4Cidr = cidr({ version: 4 });
const result1 = ipv4Cidr.parse("192.0.2.0/24");
if (result1.success) {
result1.value.version; // 4
}
const result2 = ipv4Cidr.parse("2001:db8::/32");
if (!result2.success) {
result2.error; // "Expected a valid CIDR notation, but got 2001:db8::/32."
}IP address validation
The parser delegates IP address validation to ipv4() and ipv6() parsers, so you can pass IPv4 and IPv6 options:
// Public IP ranges only
const publicCidr = cidr({
ipv4: { allowPrivate: false },
ipv6: { allowLinkLocal: false, allowUniqueLocal: false },
});
const result1 = publicCidr.parse("192.168.0.0/24");
if (!result1.success) {
result1.error; // "192.168.0.0 is a private IP address."
}Return value
The parser returns a CidrValue object with three fields:
address: The normalized IP address (string)prefix: The prefix length (number)version: The IP version (4 or 6)
const parser = cidr();
const result = parser.parse("192.0.2.0/24");
if (result.success) {
const { address, prefix, version } = result.value;
console.log(`${address}/${prefix} (IPv${version})`);
// "192.0.2.0/24 (IPv4)"
}IPv6 addresses are normalized to canonical form (lowercase, compressed):
const parser = cidr();
const result = parser.parse("2001:0DB8:0000:0000:0000:0000:0000:0000/32");
if (result.success) {
result.value.address; // "2001:db8::"
result.value.prefix; // 32
}The parser uses "CIDR" as its default metavar.
path() parser
The path() parser validates file system paths with comprehensive options for existence checking, type validation, and file extension filtering. Unlike other built-in value parsers, path() is provided by the @optique/run package since it uses Node.js file system APIs.
import { path } from "@optique/run/valueparser";
// Basic path parser (any path, no validation)
const configPath = path({ metavar: "CONFIG" });
// File must exist
const inputFile = path({ metavar: "FILE", mustExist: true, type: "file" });
// Directory must exist
const outputDir = path({ metavar: "DIR", mustExist: true, type: "directory" });
// Config files with specific extensions
const configFile = path({
metavar: "CONFIG",
mustExist: true,
type: "file",
extensions: [".json", ".yaml", ".yml"]
});Path validation options
The path() parser accepts comprehensive configuration options:
interface PathOptions {
metavar?: string; // Custom metavar (default: "PATH")
mustExist?: boolean; // Path must exist on filesystem (default: false)
mustNotExist?: boolean; // Path must not exist (default: false)
type?: "file" | "directory" | "either"; // Expected path type (default: "either")
allowCreate?: boolean; // Allow creating new files (default: false)
extensions?: string[]; // Allowed file extensions (e.g., [".json", ".txt"])
}NOTE
The mustExist and mustNotExist options are mutually exclusive. You cannot set both to true at the same time—TypeScript will catch this as a compile-time error.
Existence validation
When mustExist: true, the parser verifies that the path exists on the file system:
import { path } from "@optique/run/valueparser";
// Must exist (file or directory)
const existingPath = path({ mustExist: true });
// Must be an existing file
const existingFile = path({
mustExist: true,
type: "file",
metavar: "INPUT_FILE"
});
// Must be an existing directory
const existingDir = path({
mustExist: true,
type: "directory",
metavar: "OUTPUT_DIR"
});Non-existence validation
When mustNotExist: true, the parser rejects paths that already exist on the file system. This is useful for output files where you want to prevent accidental overwrites:
import { path } from "@optique/run/valueparser";
// Output file must not exist (prevent accidental overwrites)
const outputFile = path({
mustNotExist: true,
type: "file",
metavar: "OUTPUT_FILE"
});
// Output file with extension validation
const reportFile = path({
mustNotExist: true,
extensions: [".json", ".csv"],
metavar: "REPORT"
});
// Combine with allowCreate to also check parent directory
const logFile = path({
mustNotExist: true,
allowCreate: true,
metavar: "LOG_FILE"
});File creation validation
With allowCreate: true, the parser validates that the parent directory exists for new files:
import { path } from "@optique/run/valueparser";
// Allow creating new files (parent directory must exist)
const newFile = path({
allowCreate: true,
type: "file",
metavar: "LOG_FILE"
});
// Combination: file can be new or existing
const flexibleFile = path({
type: "file",
allowCreate: true,
extensions: [".log", ".txt"]
});Extension filtering
Restrict file paths to specific extensions using the extensions option:
import { path } from "@optique/run/valueparser";
// Configuration files only
const configFile = path({
mustExist: true,
type: "file",
extensions: [".json", ".yaml", ".yml", ".toml"],
metavar: "CONFIG_FILE"
});
// Image files only
const imageFile = path({
mustExist: true,
type: "file",
extensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
metavar: "IMAGE_FILE"
});
// Script files (existing or new)
const scriptFile = path({
allowCreate: true,
type: "file",
extensions: [".js", ".ts", ".py", ".sh"],
metavar: "SCRIPT"
});Error messages
The path() parser provides specific error messages for different validation failures:
$ myapp --config "nonexistent.json"
Error: Path nonexistent.json does not exist.
$ myapp --input "directory_not_file"
Error: Expected a file, but directory_not_file is not a file.
$ myapp --config "config.txt"
Error: Expected file with extension .json, .yaml, .yml, got .txt.
$ myapp --output "new_file/in/nonexistent/dir.txt"
Error: Parent directory new_file/in/nonexistent does not exist.
$ myapp --output "existing_file.txt"
Error: Path existing_file.txt already exists.Git integration
See the Git integration page for documentation on using Git reference parsers with Optique.
Temporal integration
See the Temporal integration page for documentation on using Temporal API parsers with Optique.
Zod integration
See the Zod integration page for documentation on using Zod schemas with Optique.
Valibot integration
See the Valibot integration page for documentation on using Valibot schemas with Optique.
Creating custom value parsers
When the built-in value parsers don't meet your needs, you can create custom value parsers by implementing the ValueParser<T> interface. Custom parsers integrate seamlessly with Optique's type system and provide the same error handling and help text generation as built-in parsers.
ValueParser interface
The ValueParser<M, T> interface defines three required properties plus a mode marker:
interface ValueParser<M extends Mode, T> {
readonly $mode: M;
readonly metavar: NonEmptyString;
parse(input: string): ModeValue<M, ValueParserResult<T>>;
format(value: T): string;
}metavar- The placeholder text shown in help messages (usually uppercase). Must be a non-empty string—TypeScript will reject empty string literals at compile time, and factory functions will throw
TypeErrorat runtime if given an empty string. parse()- Converts string input to typed value or returns error
format()- Converts typed value back to string for display
Basic custom parser
Here's a simple custom parser for IPv4 addresses:
import { message } from "@optique/core/message";
import type { ValueParser, ValueParserResult } from "@optique/core/valueparser";
interface IPv4Address {
octets: [number, number, number, number];
toString(): string;
}
function ipv4(): ValueParser<"sync", IPv4Address> {
return {
$mode: "sync",
metavar: "IP_ADDRESS",
parse(input: string): ValueParserResult<IPv4Address> {
const parts = input.split('.');
if (parts.length !== 4) {
return {
success: false,
error: message`Expected IPv4 address in format a.b.c.d, but got ${input}.`
};
}
const octets: number[] = [];
for (const part of parts) {
const num = parseInt(part, 10);
if (isNaN(num) || num < 0 || num > 255) {
return {
success: false,
error: message`Invalid IPv4 octet: ${part}. Must be 0-255.`
};
}
octets.push(num);
}
return {
success: true,
value: {
octets: octets as [number, number, number, number],
toString() {
return octets.join('.');
}
}
};
},
format(value: IPv4Address): string {
return value.toString();
}
};
}Parser with options
More sophisticated parsers can accept configuration options:
import { message } from "@optique/core/message";
import type {
NonEmptyString,
ValueParser,
ValueParserResult,
} from "@optique/core/valueparser";
interface DateParserOptions {
metavar?: NonEmptyString;
format?: 'iso' | 'us' | 'eu';
allowFuture?: boolean;
}
function date(options: DateParserOptions = {}): ValueParser<"sync", Date> {
const { metavar = "DATE", format = 'iso', allowFuture = true } = options;
return {
$mode: "sync",
metavar,
parse(input: string): ValueParserResult<Date> {
let date: Date;
// Parse according to format
switch (format) {
case 'iso':
date = new Date(input);
break;
case 'us':
// MM/DD/YYYY format
const usMatch = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (!usMatch) {
return {
success: false,
error: message`Expected US date format MM/DD/YYYY, but got ${input}.`
};
}
date = new Date(parseInt(usMatch[3]), parseInt(usMatch[1]) - 1, parseInt(usMatch[2]));
break;
case 'eu':
// DD/MM/YYYY format
const euMatch = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (!euMatch) {
return {
success: false,
error: message`Expected EU date format DD/MM/YYYY, but got ${input}.`
};
}
date = new Date(parseInt(euMatch[3]), parseInt(euMatch[2]) - 1, parseInt(euMatch[1]));
break;
}
// Validate parsed date
if (isNaN(date.getTime())) {
return {
success: false,
error: message`Invalid date: ${input}.`
};
}
// Check future constraint
if (!allowFuture && date > new Date()) {
return {
success: false,
error: message`Future dates not allowed, but got ${input}.`
};
}
return { success: true, value: date };
},
format(value: Date): string {
switch (format) {
case 'iso':
return value.toISOString().split('T')[0];
case 'us':
return `${value.getMonth() + 1}/${value.getDate()}/${value.getFullYear()}`;
case 'eu':
return `${value.getDate()}/${value.getMonth() + 1}/${value.getFullYear()}`;
}
}
};
}
// Usage
const birthDate = date({
format: 'us',
allowFuture: false,
metavar: "BIRTH_DATE"
});Integration with parsers
Custom value parsers work seamlessly with Optique's parser combinators:
import { object } from "@optique/core/constructs";
import { parse } from "@optique/core/parser";
import { argument, option } from "@optique/core/primitives";
import { integer } from "@optique/core/valueparser";
const serverConfig = object({
address: option("--bind", ipv4()),
startDate: argument(date({ format: 'iso' })),
port: option("-p", "--port", integer({ min: 1, max: 0xffff }))
});
// Full type safety with custom types
const config = parse(serverConfig, [
"--bind", "192.168.1.100",
"--port", "8080",
"2023-12-25"
]);
if (config.success) {
console.log(`Binding to ${config.value.address.toString()}:${config.value.port}.`);
console.log(`Start date: ${config.value.startDate.toDateString()}.`);
}Validation best practices
When creating custom value parsers, follow these best practices:
Clear error messages
Provide specific, actionable error messages that help users understand what went wrong:
// Good: Specific and helpful
return {
success: false,
error: message`Expected IPv4 address in format a.b.c.d, but got ${input}.`
};
// Bad: Vague and unhelpful
return {
success: false,
error: message`Invalid input.`
};
Comprehensive validation
Validate all aspects of your input format:
parse(input: string): ValueParserResult<T> {
// 1. Format validation
if (!correctFormat(input)) {
return { success: false, error: formatError };
}
// 2. Range/constraint validation
if (!withinBounds(input)) {
return { success: false, error: boundsError };
}
// 3. Semantic validation
if (!semanticallyValid(input)) {
return { success: false, error: semanticError };
}
return { success: true, value: parseValue(input) };
}
Consistent metavar naming
Use descriptive, uppercase metavar names that clearly indicate the expected input:
// Good metavar examples
metavar: "EMAIL"
metavar: "FILE"
metavar: "PORT"
metavar: "UUID"
// Poor metavar examples
metavar: "input"
metavar: "value"
metavar: "thing"Custom value parsers extend Optique's type safety and validation capabilities to handle domain-specific data types while maintaining consistency with the built-in parsers and providing excellent integration with TypeScript's type system.
Async value parsers
This API is available since Optique 0.9.0.
Value parsers can operate in either synchronous ("sync") or asynchronous ("async") mode. The mode is declared via the $mode property, which affects the return types of the parse() and suggest() methods.
All built-in value parsers are synchronous, but you can create async parsers for scenarios like:
- Validating values against a remote API
- Reading configuration from external sources
- Performing I/O-based validation (e.g., checking DNS records)
Creating async value parsers
An async value parser declares $mode: "async" and returns a Promise from its parse() method:
import { type ValueParser, type ValueParserResult } from "@optique/core/valueparser";
import { message } from "@optique/core/message";
// An async value parser that validates a URL by checking if it's reachable
function reachableUrl(): ValueParser<"async", URL> {
return {
$mode: "async",
metavar: "URL",
async parse(input: string): Promise<ValueParserResult<URL>> {
// First validate URL format
let url: URL;
try {
url = new URL(input);
} catch {
return {
success: false,
error: message`Invalid URL format: ${input}.`,
};
}
// Then check if the URL is reachable
try {
const response = await fetch(url, { method: "HEAD" });
if (!response.ok) {
return {
success: false,
error: message`URL ${input} returned status ${response.status.toString()}.`,
};
}
} catch (e) {
return {
success: false,
error: message`Could not reach URL ${input}.`,
};
}
return { success: true, value: url };
},
format(value: URL): string {
return value.toString();
},
};
}Mode propagation
When you use an async value parser with primitives and combinators, the mode automatically propagates through the parser tree. If any value parser in a composite parser is async, the entire parser becomes async:
// This parser is async because reachableUrl() is async
const parser = object({
endpoint: option("--endpoint", reachableUrl()), // async
name: option("-n", "--name", string()), // sync
});
// parser.$mode is "async"The mode is tracked at compile time through TypeScript's type system, ensuring type safety when working with async parsers.
Parsing with async parsers
For async parsers, use parseAsync() instead of parse():
// parseAsync() returns a Promise
const result = await parseAsync(parser, ["--endpoint", "https://api.example.com"]);
if (result.success) {
console.log(`Connecting to ${result.value.endpoint.toString()}.`);
}The parseAsync() function works with both sync and async parsers, always returning a Promise. For sync-only parsers, use parseSync() which returns the result directly.
Async suggestions
Async value parsers can also provide async completion suggestions by returning an AsyncIterable from the suggest() method:
import type { Suggestion, ValueParser, ValueParserResult } from "@optique/core";
import { message } from "@optique/core/message";
// A value parser that suggests valid user IDs from a remote service
function userId(): ValueParser<"async", string> {
return {
$mode: "async",
metavar: "USER_ID",
async parse(input: string): Promise<ValueParserResult<string>> {
// Validate against remote service...
return { success: true, value: input };
},
format(value: string): string {
return value;
},
async *suggest(prefix: string): AsyncIterable<Suggestion> {
// Fetch matching user IDs from remote service
const response = await fetch(
`https://api.example.com/users?prefix=${encodeURIComponent(prefix)}`
);
const users = await response.json() as { id: string; name: string }[];
for (const user of users) {
yield {
kind: "literal",
text: user.id,
description: message`${user.name}`,
};
}
},
};
}Similarly, use suggestAsync() to get suggestions from async parsers:
// suggestAsync() returns a Promise with an array of suggestions
const suggestions = await suggestAsync(parser, ["us"]);
for (const suggestion of suggestions) {
if (suggestion.kind === "literal") {
console.log(suggestion.text);
}
}Completion suggestions
This API is available since Optique 0.6.0.
Value parsers can implement an optional suggest() method to provide intelligent completion suggestions for shell completion. This method enables users to discover valid values by pressing Tab, improving usability and reducing input errors.
Built-in parser suggestions
Many built-in value parsers automatically provide completion suggestions:
import { choice, locale, url } from "@optique/core/valueparser";
import { timeZone } from "@optique/temporal";
// Choice parser suggests all available options
const format = choice(["json", "yaml", "xml"]);
// Completing "j" suggests: ["json"]
// URL parser suggests protocol completions when allowedProtocols is set
const apiUrl = url({ allowedProtocols: ["https:", "http:"] });
// Completing "ht" suggests: ["http://", "https://"]
// Locale parser suggests common locale identifiers
const userLocale = locale();
// Completing "en" suggests: ["en", "en-US", "en-GB", "en-CA", ...]
// Timezone parser uses Intl.supportedValuesOf for dynamic suggestions
const timezone = timeZone();
// Completing "America/" suggests: ["America/New_York", "America/Chicago", ...]Custom suggestion implementation
Implement the suggest() method in custom value parsers to provide domain-specific completions:
import { type ValueParser, type ValueParserResult } from "@optique/core/valueparser";
import { type Suggestion } from "@optique/core/parser";
import { message } from "@optique/core/message";
function httpMethod(): ValueParser<"sync", string> {
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
return {
$mode: "sync",
metavar: "METHOD",
parse(input: string): ValueParserResult<string> {
const method = input.toUpperCase();
if (methods.includes(method)) {
return { success: true, value: method };
}
return {
success: false,
// Note: For proper formatting of choice lists, see the "Formatting choice lists"
// section in the Concepts guide on Messages
error: message`Invalid HTTP method: ${input}. Valid methods: ${methods.join(", ")}.`,
};
},
format(value: string): string {
return value;
},
*suggest(prefix: string): Iterable<Suggestion> {
for (const method of methods) {
if (method.toLowerCase().startsWith(prefix.toLowerCase())) {
yield {
kind: "literal",
text: method,
description: message`HTTP ${method} request method`
};
}
}
},
};
}File completion delegation
For file and directory inputs, delegate completion to the shell's native file system integration using file-type suggestions:
import { type ValueParser, type ValueParserResult } from "@optique/core/valueparser";
import { type Suggestion } from "@optique/core/parser";
import { message } from "@optique/core/message";
function configFile(): ValueParser<"sync", string> {
return {
$mode: "sync",
metavar: "CONFIG",
parse(input: string): ValueParserResult<string> {
// Validation logic here
return { success: true, value: input };
},
format(value: string): string {
return value;
},
*suggest(prefix: string): Iterable<Suggestion> {
yield {
kind: "file",
type: "file",
extensions: [".json", ".yaml", ".yml"],
includeHidden: false,
description: message`Configuration file`
};
},
};
}The suggest() method receives the current input prefix and should yield Suggestion objects. The shell completion system handles filtering and display, while native file completion provides better performance and platform-specific behavior for file system operations.
Completion suggestions improve user experience by making CLI applications more discoverable and reducing typing errors, while maintaining the same type safety and validation guarantees as the parsing logic.