Skip to content

Value and environment sources

Command-line arguments are one source of configuration. @bomb.sh/router also binds JavaScript values, such as the contents of a configuration file, and environment variables onto the same route models. Every source goes through the same schema, so a value from the environment is validated exactly like a value from argv.

Give parse() a list of value sources and a list of environment sources. The name of each source labels where its data came from:

import
var process: NodeJS.Process
process
from "node:process";
import {
function command<const N extends string, const E extends readonly Input[]>(start: Definition<N>, ...elements: E & Validate<CommandZero<N>, E>): Materialize<Fold<CommandZero<N>, Normalize<E>>>
command
,
function name<N extends string>(name: N): Definition<N>
name
,
function option<const N extends string, const E extends readonly Unary[]>(named: Definition<N>, ...elements: E & Check<Zero<N>, E>): ElementOf<...>
option
,
function parse<const R extends AnyRoute>(route: R, input: Input): Parse<R>
parse
,
function route<const N extends string, const E extends readonly Input[]>(start: Definition<N>, ...elements: E & Validate<RouteZero<N>, E>): Materialize<Fold<RouteZero<N>, Normalize<E>>>
route
,
function schema<S extends Schema>(schema: S): TransformElement<SchemaTransform<OutputOf<S>>>
schema
,
} from "@bomb.sh/router";
import * as
import z
z
from "zod";
const
const app: Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>
app
=
command<"gh", readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>(start: Definition<"gh">, elements_0: Route<...>): Route<...>
command
(
name<"gh">(name: "gh"): Definition<"gh">
name
("gh"),
route<"pr", readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>(start: Definition<"pr">, elements_0: Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>): Route<...>
route
(
name<"pr">(name: "pr"): Definition<"pr">
name
("pr"),
command<"list", readonly [ModelElement<ParamModel<"limit", number>>]>(start: Definition<"list">, elements_0: ModelElement<ParamModel<"limit", number>>): Route<...>
command
(
name<"list">(name: "list"): Definition<"list">
name
("list"),
option<"limit", readonly [TransformElement<SchemaTransform<number>>]>(named: Definition<"limit">, elements_0: TransformElement<SchemaTransform<number>>): ModelElement<...>
option
(
name<"limit">(name: "limit"): Definition<"limit">
name
("limit"),
schema<z.ZodDefault<z.ZodNumber>>(schema: z.ZodDefault<z.ZodNumber>): TransformElement<SchemaTransform<number>>
schema
(
import z
z
.
function number(params?: z.RawCreateParams & {
coerce?: boolean;
}): z.ZodNumber
export number
number
().
ZodType<number, ZodNumberDef, number>.default(def: number): z.ZodDefault<z.ZodNumber> (+1 overload)
default
(30))),
),
),
);
declare const
const config: unknown
config
: unknown;
const
const result: Parse<Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>>
result
=
parse<Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>>(route: Route<...>, input: Input): Parse<...>
parse
(
const app: Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>
app
, {
argv: string[]
argv
:
var process: NodeJS.Process
process
.
NodeJS.Process.argv: string[]

The process.argv property returns an array containing the command-line arguments passed when the Node.js process was launched. The first element will be

execPath

. See process.argv0 if access to the original value of argv[0] is needed. The second element will be the path to the JavaScript file being executed. The remaining elements will be any additional command-line arguments.

For example, assuming the following script for process-args.js:

import { argv } from 'node:process';
// print process.argv
argv.forEach((val, index) => {
console.log(`${index}: ${val}`);
});

Launching the Node.js process as:

Terminal window
node process-args.js one two=three four

Would generate the output:

0: /usr/local/bin/node
1: /Users/mjr/work/node/process-args.js
2: one
3: two=three
4: four

@since ― v0.1.27

argv
.
Array<string>.slice(start?: number, end?: number): string[]

Returns a copy of a section of an array. For both start and end, a negative index can be used to indicate an offset from the end of the array. For example, -2 refers to the second to last element of the array.

@param ― start The beginning index of the specified portion of the array. If start is undefined, then the slice begins at index 0.

@param ― end The end index of the specified portion of the array. This is exclusive of the element at the index 'end'. If end is undefined, then the slice extends to the end of the array.

slice
(2),
values?: readonly ValueSource[]
values
: [{
name: string
name
: "config.json",
value: unknown
value
:
const config: unknown
config
}],
envs?: readonly EnvSource[]
envs
: [{
EnvSource.name: string
name
: "process",
EnvSource.value: Readonly<Record<string, string | undefined>>
value
:
var process: NodeJS.Process
process
.
NodeJS.Process.env: NodeJS.ProcessEnv

The process.env property returns an object containing the user environment. See environ(7).

An example of this object looks like:

{
TERM: 'xterm-256color',
SHELL: '/usr/local/bin/bash',
USER: 'maciej',
PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
PWD: '/Users/maciej',
EDITOR: 'vim',
SHLVL: '1',
HOME: '/Users/maciej',
LOGNAME: 'maciej',
_: '/usr/local/bin/node'
}

It is possible to modify this object, but such modifications will not be reflected outside the Node.js process, or (unless explicitly requested) to other Worker threads. In other words, the following example would not work:

Terminal window
node -e 'process.env.foo = "bar"' &#x26;&#x26; echo $foo

While the following will:

import { env } from 'node:process';
env.foo = 'bar';
console.log(env.foo);

Assigning a property on process.env will implicitly convert the value to a string. This behavior is deprecated. Future versions of Node.js may throw an error when the value is not a string, number, or boolean.

import { env } from 'node:process';
env.test = null;
console.log(env.test);
// => 'null'
env.test = undefined;
console.log(env.test);
// => 'undefined'

Use delete to delete a property from process.env.

import { env } from 'node:process';
env.TEST = 1;
delete env.TEST;
console.log(env.TEST);
// => undefined

On Windows operating systems, environment variables are case-insensitive.

import { env } from 'node:process';
env.TEST = 1;
console.log(env.test);
// => 1

Unless explicitly specified when creating a Worker instance, each Worker thread has its own copy of process.env, based on its parent thread's process.env, or whatever was specified as the env option to the Worker constructor. Changes to process.env will not be visible across Worker threads, and only the main thread can make changes that are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner unlike the main thread.

@since ― v0.1.27

env
}],
});

A value source nests parameters under their route path. An environment variable joins the route path and the parameter name in upper snake case:

RouteParameterValue sourceEnvironment variable
/pr/listlimit{ pr: { list: { limit: 50 } } }PR_LIST_LIMIT
/pr/createtitle{ pr: { create: { title: "Fix typo" } } }PR_CREATE_TITLE
/pr/createdraft{ pr: { create: { draft: true } } }PR_CREATE_DRAFT

Parameters of the root route sit at the top level of a value source, and their environment variable is the bare name, such as VERBOSE for a root verbose toggle.

To read a different environment variable for one parameter, use env().

When more than one source sets a parameter, the router uses this order:

CLI → environment → values → schema default

With a value source of { pr: { list: { limit: 50 } } } and PR_LIST_LIMIT=40:

Invocationlimit
gh pr list --limit 1010
gh pr list40
gh pr list, without PR_LIST_LIMIT50
gh pr list, without either source30

Within one kind of source, the first source in the list wins. Put the most specific source first, for example a project file before a user-wide file.

Command-line text and environment text are decoded before validation, as described in how values are decoded. So PR_LIST_LIMIT=40 reaches the schema as the number 40.

JavaScript values are used directly. A value source of { pr: { list: { limit: "50" } } } fails z.number(), because the string is not converted.

Toggles read from the environment accept exactly true or false.

withValues() and withEnvs() attach sources to a route definition instead of passing them to parse(). Use them for defaults that belong with a route:

import {
function command<const N extends string, const E extends readonly Input[]>(start: Definition<N>, ...elements: E & Validate<CommandZero<N>, E>): Materialize<Fold<CommandZero<N>, Normalize<E>>>
command
,
function name<N extends string>(name: N): Definition<N>
name
,
function option<const N extends string, const E extends readonly Unary[]>(named: Definition<N>, ...elements: E & Check<Zero<N>, E>): ElementOf<...>
option
,
function route<const N extends string, const E extends readonly Input[]>(start: Definition<N>, ...elements: E & Validate<RouteZero<N>, E>): Materialize<Fold<RouteZero<N>, Normalize<E>>>
route
,
function schema<S extends Schema>(schema: S): TransformElement<SchemaTransform<OutputOf<S>>>
schema
,
function withValues(values: readonly ValueSource[]): IdentityElement<AnyRoute>
withValues
,
} from "@bomb.sh/router";
import * as
import z
z
from "zod";
const
const app: Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>
app
=
command<"gh", readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>(start: Definition<"gh">, elements_0: Route<...>): Route<...>
command
(
name<"gh">(name: "gh"): Definition<"gh">
name
("gh"),
route<"pr", readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>(start: Definition<"pr">, elements_0: Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>): Route<...>
route
(
name<"pr">(name: "pr"): Definition<"pr">
name
("pr"),
command<"list", readonly [IdentityElement<AnyRoute>, ModelElement<ParamModel<"limit", number>>]>(start: Definition<"list">, elements_0: IdentityElement<...>, elements_1: ModelElement<ParamModel<"limit", number>>): Route<...>
command
(
name<"list">(name: "list"): Definition<"list">
name
("list"),
function withValues(values: readonly ValueSource[]): IdentityElement<AnyRoute>
withValues
([{
name: string
name
: "defaults",
value: unknown
value
: {
limit: number
limit
: 50 } }]),
option<"limit", readonly [TransformElement<SchemaTransform<number>>]>(named: Definition<"limit">, elements_0: TransformElement<SchemaTransform<number>>): ModelElement<...>
option
(
name<"limit">(name: "limit"): Definition<"limit">
name
("limit"),
schema<z.ZodDefault<z.ZodNumber>>(schema: z.ZodDefault<z.ZodNumber>): TransformElement<SchemaTransform<number>>
schema
(
import z
z
.
function number(params?: z.RawCreateParams & {
coerce?: boolean;
}): z.ZodNumber
export number
number
().
ZodType<number, ZodNumberDef, number>.default(def: number): z.ZodDefault<z.ZodNumber> (+1 overload)
default
(30))),
),
),
);

Values attached with withValues() are addressed from the route they belong to, so /pr/list reads { limit: 50 }, not { pr: { list: { limit: 50 } } }. They take precedence over values passed to parse(). Environment sources attached with withEnvs() still use the full variable name, such as PR_LIST_LIMIT.

Deno asks for permission before a program reads environment variables. If you pass process.env to parse(), run your CLI with --allow-env (-E). Parsing argv alone needs no permission.