@molt/command
🌱 Type-safe CLI command definition and execution.
Installation
npm add @molt/command
Example
import { Command } from '@molt/command'
import { z } from 'zod'
const args = Command.create({
'--file-path': z.string().describe(`Path to the file to convert.`),
'--to': z.enum(['json', ' yaml', 'toml']).describe(`Format to convert to.`),
'--from': z
.enum(['json', 'yaml', 'toml'])
.optional()
.describe(`Format to convert from. By default inferred from the file extension.`),
'--verbose -v': z.boolean().default(false).describe(`Log detailed progress as conversion executes.`),
'--move -m': z.boolean().default(false).describe(`Delete the original file after it has been converted.`),
}).parseOrThrow()
args.filePath
args.to
args.from
args.verbose
args.move
my-binary --file ./music.yaml --to json
Autogenerated help:
ts-node convert -h
PARAMETERS
Name Type / Summary Default Via Environment
to json | yaml | toml REQUIRED ✕
Format to convert to.
file-path string REQUIRED FILE_PATH
Path to the file to convert.
from json | yaml | toml Inferred from the file ✕
Format to convert from. extension.
verbose boolean false ✕
v Log detailed progress.
move boolean false ✕
v Delete the original file after
it has been converted.
h boolean false ✕
Output succinct info on how to
use this CLI.
help boolean false ✕
Output detailed info on how to
use this CLI.
Features
-
Automatic parameter parsing based on specified Zod types.
-
Normalization between camel and kebab case:
-
Parameter specs are normalized:
const args = Command.create({
'--do-it-a': z.boolean()
'--doItB': z.boolean()
doItC: z.boolean()
}).parseOrThrow()
args1.doItA
args2.doItB
args3.doItC
-
User flags are normalized:
$ binary --do-it
$ binary --doIt
-
Specify one or multiple (aka. aliases) short and long flags:
Command.create({ '-f --force --forcefully': z.boolean() }).parseOrThrow()
-
Use Zod .default(...)
method for setting default values.
const args = Command.create({ '--path': z.string().default('./a/b/c') }).parseOrThrow()
args.path === './a/b/c/'
args.path === '/over/ride'
-
Pass arguments via environment variables (customizable)
const args = Command.create({ '--path': z.string() }).parseOrThrow()
args.path === './a/b/c/'
-
Use Zod .describe(...)
for automatic docs.
-
Future feature: automatic help generation.
-
Parameter stacking e.g. binary -abc
instead of binary -a -b -c
-
Separator of =
or space, e.g. binary -a=foo -b=bar
or binary -a foo -b bar
Guide
Parameter Naming
Property Name Syntax
You can define parameters as a zod object schema using regular property names. These are flags for your CLI but arguments can also be passed by environment variables so in a way this is a neutral form that doesn't privilege either argument passing mechanism.
const args = Command.create({
foo: z.string(),
bar: z.number(),
qux: z.boolean(),
}).parseOrThrow()
args.foo
args.bar
args.qux
Flag Syntax
You can also define them using flag syntax if you prefer. Thanks to @molt/types
this style doesn't sacrifice any type safety.
const args = Command.create({
'--foo': z.string(),
'--bar': z.number(),
'--qux': z.boolean(),
}).parseOrThrow()
args.foo
args.bar
args.qux
Short, Long, & Aliasing
You can give your parameters short and long names, as well as aliases.
A set of parameter names gets normalized into its canonical name internally (again thanks to @molt/types
this is all represented statically as well). The canonical name choice is as follows:
- The first long flag
- Otherwise the first short flag
const args = Command.create({
'--foobar --foo -f ': z.string(),
'--bar -b -x': z.number(),
'-q --qux': z.boolean(),
'-m -n': z.boolean(),
}).parseOrThrow()
args.foobar === 'moo'
args.bar === 1
args.qux === true
args.m === true
If you prefer you can use a dash-prefix free syntax:
const args = Command.create({
'foobar foo f ': z.string(),
'bar b x': z.number(),
'q qux': z.boolean(),
'm n': z.boolean(),
}).parseOrThrow()
Kebab / Camel Case
You can use kebab or camel case (and likewise your users can pass flags in either style). Canonical form internally uses camel case.
const args = Command.create({
'foo-bar': z.string(),
quxLot: z.string(),
}).parseOrThrow()
args.fooBar === 'moo'
args.quxLot === 'zoo'
Parameter Typing
Parameter types via Zod schemas affect parsing in the following ways.
Boolean
- Flag does not accept any arguments.
- Environment variable accepts
"true"
or "1"
for true
and "false"
or "0"
for false
. - Negated form of parameters automatically accepted.
Examples:
const args = Command.create({ 'f force forcefully': z.boolean() }).parseOrThrow()
args.force === false
args.force === true
Number
- Flag expects an argument.
- Argument is cast via the
Number()
function.
Enum
- Flag expects an argument.
Argument Passing
This section is about users passing arguments to the parameters you've defined for your CLI.
Parameter Argument Separator
Arguments can be separated from parameters using the following characters:
Examples:
binary --foo=moo
binary --foo= moo
binary --foo = moo
binary --foo moo
Note that when =
is attached to the value side then it is considered part of the value:
binary --foo =moo
Stacked Short Flags
Boolean short flags can be stacked. Imagine you have defined three parameters a
, b
, c
. They could be passed like so:
binary -abc
The last short flag does not have to be boolean flag. For example if there were a d
parameter taking a string, this could work:
binary -abcd foobar
Environment Arguments
Parameter arguments can be passed by environment variables instead of flags.
Environment arguments have lower precedence than Flags, so if an argument is available from both places, the environment argument is ignored while the flag argument is used.
Default Name Pattern
By default environment arguments can be set using one of the following naming conventions (note: Molt reads environment variables with case-insensitivity):
CLI_PARAMETER_{parameter_name}
CLI_PARAM_{parameter_name}
const args = Command.create({ '--path': z.string() }).parseOrThrow()
args.path === './a/b/c/'
Toggling
You can toggle environment arguments on/off. It is on by default.
const command = Command.create({ '--path': z.string() }).settings({
environment: false,
})
command.parseOrThrow()
You can also toggle with the environment variable CLI_SETTINGS_READ_ARGUMENTS_FROM_ENVIRONMENT
(case insensitive):
const command = Command.create({ '--path': z.string() })
command.parseOrThrow()
Selective Toggling
You can toggle environment on for just one or some parameters.
const args = Command.create({
'--foo': z.string(),
'--bar': z.string().default('not_from_env'),
})
.settings({ environment: { foo: true } })
.parseOrThrow()
args.foo === 'foo'
args.bar === 'not_from_env'
You can toggle environment on except for just one or some parameters.
const args = Command.create({
'--foo': z.string().default('not_from_env'),
'--bar': z.string().default('not_from_env'),
'--qux': z.string().default('not_from_env'),
})
.settings({ environment: { $default: true, bar: false } })
.parseOrThrow()
args.foo === 'foo'
args.bar === 'not_from_env'
args.qux === 'qux'
Custom Prefix
You can customize the environment variable name prefix:
const args = Command.create({ '--path': z.string() })
.settings({ environment: { $default: { prefix: 'foo' } } })
.parseOrThrow()
args.path === './a/b/c/'
You can pass a list of accepted prefixes instead of just one. Earlier ones take precedence over later ones:
const args = Command.create({ '--path': z.string() })
.settings({ environment: { $default: { prefix: ['foobar', 'foo'] } } })
.parseOrThrow()
args.path === './a/b/c/'
args.path === './a/b/c/'
args.path === './a/b/c/'
Selective Custom Prefix
You can customize the environment variable name prefix for just one or some parameters.
const args = Command.create({
'--foo': z.string().default('not_from_env'),
'--bar': z.string().default('not_from_env'),
'--qux': z.string().default('not_from_env'),
})
.settings({ environment: { bar: { prefix: 'MOO' } } })
.parseOrThrow()
args.foo === 'foo'
args.bar === 'bar'
args.qux === 'qux'
You can customize the environment variable name prefix except for just one or some parameters.
const args = Command.create({
'--foo': z.string().default('not_from_env'),
'--bar': z.string().default('not_from_env'),
'--qux': z.string().default('not_from_env'),
})
.settings({ environment: { $default: { enabled: true, prefix: 'MOO' }, bar: { prefix: true } } })
.parseOrThrow()
args.foo === 'foo'
args.bar === 'bar'
args.qux === 'qux'
Prefix Disabling
You can remove the prefix altogether. Pretty and convenient, but be careful for unexpected use of variables in host environment that would affect your CLI execution!
const args = Command.create({ '--path': z.string() })
.settings({ environment: { $default: { prefix: false } } })
.parseOrThrow()
args.path === './a/b/c/'
Selective Prefix Disabling
You can disable environment variable name prefixes for just one or some parameters.
const args = Command.create({
'--foo': z.string().default('not_from_env'),
'--bar': z.string().default('not_from_env'),
'--qux': z.string().default('not_from_env'),
})
.settings({ environment: { bar: { prefix: false } } })
.parseOrThrow()
args.foo === 'foo'
args.bar === 'bar'
args.qux === 'qux'
You can disable environment variable name prefixes except for just one or some parameters.
const args = Command.create({
'--foo': z.string().default('not_from_env'),
'--bar': z.string().default('not_from_env'),
'--qux': z.string().default('not_from_env'),
})
.settings({ environment: { $default: { enabled: true, prefix: false }, bar: { prefix: true } } })
.parseOrThrow()
args.foo === 'foo'
args.bar === 'bar'
args.qux === 'qux'
Case Insensitive
Environment variables are considered in a case insensitive way so all of these work:
const args = Command.create({ '--path': z.string() }).parseOrThrow()
args.path === './a/b/c/'
Validation
By default, when a prefix is defined, a typo will raise an error:
const command = Command.create({ '--path': z.string() })
command.parseOrThrow()
If you pass arguments for a parameter multiple times under different environment variable name aliases an error will be raised.
const command = Command.create({ '--path': z.string() })
/ole/ Throws error because user intent is ambiguous.
command.parseOrThrow()