It doesn't look like much, and it isn't but it's quite handy. The basic idea is that you provide a config object, and get back a function which you pass the args in, and you'll get an object back which resembles the config object, except populated with the values fetched from the command line. And the greatest thing - it's fully typed! (yea don't try to make sense of the typescript bit, you'll be busy for hours)
The process is divided into two parts: Configuring and Parsing. Watch this:
// node ./weirdcompiler.js ./file.txt -i ./libs/lib1,./libs/lib2,./libs/lib3 -o ./out.txt --lang Java
const arguments = parse({
include: {
short: 'i',
long: 'include',
format: format.csv(format.path(true)),
default: []
},
out: {
short: 'o',
format: format.path(false),
default: null
},
language: {
short: 'l',
long: 'lang',
format: format.oneof(['c', 'c++', 'js', 'ts', 'java'], false)
}
}, format.path(true))(process.argv.slice(2)) // slice the first two off, so we don't get the node executable and the current file
// arguments: {default: './file.txt', include: ['./libs/lib1', './libs/lib2', './libs/lib3], out: './out.txt', language: 'java'}
Simple. The keys of the config object are used as the names of the options. the values are configurations for each option.
You must provide at least one of short
or long
. These represent what flags will be assigned to each option.
the short
option will match any -{letter}
option, and long
matches --{name}
. It's worth noting that short
can only contain one character.
Next, each option can define a format
option. This determines the matcher to be used when parsing the option. For instance, if format
is set to format.Int
,
the following would return a number
: --some-option 10
, whereas providing a non-integer would yield a type error: --some-option eggs
format
is optional. If omitted, the type is inferred to boolean
, this means that no value is expected. If no format is provided, short
options can be aggregated.
-omg
, would set each option whose short
properties are either o
, m
, or g
to the inverse of their default
option (which is false
if omitted).
For instance, the following config:
parse({
open: {
short: 'o',
default: true
}
})(['-o'])
would set its open
value to false
, such that allowing --no-...
options is possible.
If the default
option is omitted, the parameter is considered required. Failing to provide it through the args will cause an error.
The type of default
must be assignable to ReturnType<format>
, meaning the type of default
, but match any type that came out of the format
parser.
Typescript will complain if not.
CLI options can come in many shapes and sizes, that's why converting them to a more useful format is a pretty important job of a CLI parser.
The system employed here is designed to be scalable. The only thing required to define your own is explicit typing. The format.ts
file contains a bunch of examples which you can use to create your own.
It's a good idea to throw errors when the string cannot be parsed to the correct type, as this will inform users that they are idiots and didn't put in the right value.
Format functions go into the format
property of options, and take a single string as an input, and can return any non-void value. Defining a custom parser can be done like so:
const args = parse({
datetime: {
...
format(arg: string): Date { return new Date(arg) }
}
})([..., '12-10-21'])
You may have noticed how some command lines will take the first parameter and infer its property name automatically. There is similar functionality here.
In the parse
function, the second parameter specifies a parser for the default
option, which if omitted makes the default option redundant.
It's value is placed into the default
key of the returned option map.
const args = parse({}, formats.Int)['0'] // {default: 0}
All in 70 lines of TS. Enjoy