Reargs
RegExp based command line arguments parser.
So why another argument parser library ?
None of the ones I found leverage the power of RegExp !
With great power comes great responsability !
$ prog -m swagger dump api from https://api.endpoint.io -- do not parse
Or having multiple ways of asking for the same action:
$ prog -s
$ prog sh
$ prog shell
Last but not least, I wanted to have a descriptive help text out of the box. Thanks to nunjucks you just have to call generateHelp to have the magic
How does it work ?
Behind the scenes are Regular Expressions - hence the name - and the fact that they are both very flexible and more than enough for what we want.
Instantiation
// CommonJS
const Reargs = require('reargs')
const myArgs = new Reargs(params = {}, opts = null, debug = false)
// ES2016
import Reargs from 'reargs'
const myArgs = new Reargs(params = {}, opts = null, debug = false)
Parameters
This object contains all the parameters, which have the following properties:
attribute | optional ? | accepted types | default value | meaning | example |
---|---|---|---|---|---|
group | yes | string | _ |
groups commands, see note below | 'command' |
short | yes if long option is set |
string or RegExp | '' | short version of parameter | '-a' |
long | yes if short option is set |
string or RegExp | '' | longer version of parameter | '--append' |
humanReadable | yes | string | autogenerated from short and long properties |
human readable version of the parameters for help | '-a, --append' |
help | yes | string | 'no help' | help text | 'append to file' |
stopParse | yes | boolean | false | stop parsing when found (see note below) | |
hidden | yes | boolean | false | do not show parameter if default help template (see note below) | |
multiple | yes | boolean | false | accept multiple occurrences of a parameter (see below) | |
captureMultiple | yes | string or RegExp | undefined | allow capture of multiple values inside a parameter (see below) | |
values | yes | any including Object | false | default value for the parameter | see notes below |
A typical example for help
would be:
const params = {
help: {
group: 'command',
short: '-h',
long: '--help',
humanReadable: '-h, --help',
help: 'this help',
stopParse: true,
hidden: false,
multiple: false,
// captureMultiple: '',
values: false
}
}
Indeed, no default help entry is provided. If you want to provide such functionality (which you might want), then you have to set an entry like the one above.
Notes
- All commands can be grouped together using the
group
property. It can be useful to distinguish betweencommand
s andoption
s for example. - All arguments can have two forms,
short
andlong
. Although they do not have to be shorter or longer one to the other, you can provide alternate versions if you need. At least one of them is required, an exception is thrown otherwise. - These properties can be
string
or instances ofRegExp
. If you prefer using strings, please make sure you use double backslashs to escape. E.g. the following are equivalent:
const exampleWithStrings = {
help: {
help: 'this help or additional help on a given topic',
short: '((?<topic>[\\w|\\/]+) )?-h',
long: 'help( (?<topic>[\\w|\\/]+))?',
}
}
const exampleWithRegExp = {
help: {
help: 'this help or additional help on a given topic',
short: /((?<topic>[\w|/]+) )?-h/,
long: /help( (?<topic>[\w|/]+))?/,
}
}
Notes
A humanReadable
text is computed from both short
and long
inputs (with the help of an option longShortDelimiter). If this does not suit the way you want it to appear when calling generateHelp, feel free to customize !
The stopParse
parameter implies that everything after this parameter is discarded from the input.
See it as the usual --
in your shell.
The hidden
flag will still parse the parameter but it will not be shown in the help generated with generateHelp. Of course you will have to implement the same logic if you want to customize your help output.
See Help templates below.
Options
A few options are also available to adjust Reargs
behavior. Below are the default values:
const opts = {
longShortDelimiter: '\n',
paramDescriptionSpacer: '.',
prePaddingSpaces: 2,
alignLongIfNoShort: 4,
exitOnStop: false,
nunjucksAutoEscape: false,
}
-
longShortDelimiter
will be used to generate thehumanReadable
documentation fromjoin
ing bothshort
andlong
parameters variations. -
paramDescriptionSpacer
is also used when calling generateHelp, and changes the spacing character used between parameters and the related help. -
prePaddingSpaces
adds that many spaces ( -
alignLongIfNoShort
adds that many spaces before arguments that have noshort
property, to have a cleaner alignment with arguments having bothshort
andlong
properties set. -
exitOnStop
if set, stops immediately parsing when a parameter withstopParse
property set to true is found, not evaluating anything else. -
nunjucksAutoEscape
is set to false by default inReargs
but by default set to true upstream. Since we are dealing with CLI, there should not be any mishaps by disabling safety guards.
Note Although discouraged,
paramDescriptionSpacer
andprePaddingSpaces
can be overriden ultimately in the template, because they are arguments of padEndJinja2
-like modifier.
Debug
To help with debugging your configuration, a debug mechanism has been set up.
At several places in the code, some debug messages will be generated. Depending on the value of debug
they will be outputted ... or not.
debug value | meaning |
---|---|
false (default) | no debug output at all |
true | debug will be output with console.log
|
(...) => { do_whatever_you_want() } |
a custom function taking as many arguments like console.log to suit your needs. |
Parsing
Parsing is done as simply calling the following code :
const unparsable = myArgs.parse(process.argv.slice(2))
Yep that's it ! It takes an Array
as input argument, and returns the unparsed tokens.
Note The difference between
unparsed
andremain
lies in thatremain
has never been given a chance to be parsed. On the contrary, what is returned by theparse
function has been given a chance to be parsed, unsuccesfully. Bothremain
andunparsable
will be returned as string with\x00
special character. This is by design to allow arguments to have spaces in them. When you want to deal with positional arguments, you just have tosplit('\x00')
to get a properArray
for whatever your needs would be.
Retrieving values
There are three different functions to retrieve the result after a call to parse.
- getValue : get the value of a single argument (and if present the capture group)
- getGroupValues : get all the values from a specific group (see groups)
- getAllValues : as the name suggests, retrieve all the values from all the parameters
Note By design, values are flattened to a simple Object. This means that if you happen to have multiple values having the same name, unexpected behavior can arise. This is done to avoid confusing and usually this should not happen (or must not happen!).
const value = myArgs.getValue(askforId, captureGroupName = null)
const groupValues = myArgs.getGroupValues(group)
const allValues = myArgs.getAllValues()
Let's go with an example. Consider this little snippet :
// Definition
const myArgs = new Reargs({
help: {
help: 'this help or additional help on a given topic',
short: '((?<topic>[\\w|\\/]+) )?-h',
long: 'help( (?<topic>[\\w|\\/]+))?',
humanReadable: 'help [topic], [topic] -h'
}
})
const unparsable = myArgs.parse(process.argv.slice(2))
const valueOfParam = myArgs.getValue('help', 'topic')
The capturing group topic
will store the value, if present.
If the program is called with only help
as argument, the following will be true:
const topic = myArgs.getValue('help', 'topic') // topic: undefined
const help = myArgs.getValue('help') // returns { topic: undefined }
const groupValues = myArgs.getGroupValues('_') // returns { topic: undefined }
const allValues = myArgs.getAllValues() // returns { topic: undefined }
Notes
- If an argument has a capturing group, its value is propagated
- If no value could be found on the command line the default one, if it exists, will be propagated instead
- You can have the same name between capturing group name and parameter name
Default values
Now let's consider the following example with default values :
// Definition
const myArgs = new Reargs({
help: {
help: 'this help or additional help on a given topic',
short: '((?<topic>[\\w|\\/]+) )?-h',
long: 'help( (?<topic>[\\w|\\/]+))?',
humanReadable: 'help [topic], [topic] -h',
values: {
topic: 'general'
}
}
})
const unparsable = myArgs.parse(process.argv.slice(2))
const valueOfParam = myArgs.getValue('help', 'topic')
If the program is called with only help
as argument, the following will be true:
const topic = myArgs.getValue('help', 'topic') // topic: 'general'
const help = myArgs.getValue('help') // returns { topic: 'general' }
const groupValues = myArgs.getGroupValues('_') // returns { topic: 'general' }
const allValues = myArgs.getAllValues() // returns { topic: 'general' }
Multiple values
Sometimes you want to capture multiple values, let's see two examples of parsing key:value
pairs.
A single capture group, multiple arguments
Consider the following command line arguments :
$ prog -e key1:value1 -e key2:value2 -e key3: -e :value4
The definition would be :
// Definition
const oneKvPerParameter = {
kv1: {
help: 'set key value pairs one at a time',
short: '-e (?<key>[\\w]+)?:(?<value>[\\w]+)?',
humanReadable: '-e key:value',
multiple: true,
values: {
key: 'defaultKey',
value: 'defaultValue'
}
}
})
When multiple
property is set to false
(default), only the latest occurrence of that parameter is taken into account, previous occurrences are simply discarded.
const kv1keys = myArgs.getValue('kv1', 'key')
// ['defaultKey']
const kv1 = myArgs.getValue('kv1')
// {
// key: ['defaultKey'],
// value: ['value4']
// }
If you set multiple: true
then the following will happen :
const kv1keys = myArgs.getValue('kv1', 'key')
// ['key1', 'key2', 'key3', 'defaultKey']
const kv1 = myArgs.getValue('kv1')
// {
// key: ['key1', 'key2', 'key3', 'defaultKey'],
// value: ['value1', 'value2', 'defaultValue', 'value4']
// }
Note If you need mapping between keys and values, this work must be done in your program.
Mutiple capture groups in a single argument
The situation is a bit different :
$ prog -a key1:value1,key2:value2,key3:,:value4
The definition would be :
// Definition
const manyKvPerParameter = {
kv2: {
help: 'set key value pairs all together',
short: '-a ',
humanReadable: '-a key:value,key:value,...',
captureMultiple: '(?<key2>[\\w]+)?:(?<value2>[\\w]+)?,?',
values: {
key2: 'defaultKey2',
value2: 'defaultValue2'
}
}
}
Note As you can see the
short
property has aspace at the end. This is to force a space between
-a
and the rest of parameter attributes.
And therefore:
const kv2keys = myArgs.getValue('kv2', 'key')
// ['key1', 'key2', 'key3', 'defaultKey']
const kv2 = myArgs.getValue('kv2')
// {
// key: ['key1', 'key2', 'key3', 'defaultKey'],
// value: ['value1', 'value2', 'defaultValue', 'value4']
// }
Combining both !
And yes the previous situations, can be generalized !
$ prog -b key0:value0,key1:value2 -b key1:value1,key2:value2,key3:,:value4
The definition would be :
// Definition
kv3: {
help: 'set key value pairs all together ... multiple times !',
short: '-b ',
humanReadable: '-b key:value,key:value,...',
captureMultiple: '(?<key3>[\\w]+)?:(?<value3>[\\w]+)?,?',
multiple: true,
values: {
key3: 'defaultKey3',
value3: 'defaultValue3'
}
}
And therefore:
const kv3keys = myArgs.getValue('kv3', 'key')
// ['key0', 'key1', 'key2', 'key3', 'defaultKey']
const kv3 = myArgs.getValue('kv3')
// {
// key: ['key0', 'key1', 'key2', 'key3', 'defaultKey'],
// value: ['value0', 'value2', 'value1', 'value2', 'defaultValue', 'value4']
// }
Note Here again,
Reargs
do not make any assumption on whether keys or values should be merged and how (e.g double definition of the keykey1
). It is up to the responsability of the caller to manage that.
Generate Help
As you have seen, many options and properties are meant to ease the generation of help message.
Everything is handled by the generateHelp with the following parameters:
parameter name | default value | description | example |
---|---|---|---|
contextHelp | {} | some properties can be overriden by default and you can add your own custom variables as well but you will have to use your own template | { name: "foobar", version: "1.0rc1" } |
templateSource | defaultHelpTemplate | a nunjucks template | you can view the default below |
// This will generate a help message with all parameters but default context values using the default template.
const helpMessage = myArgs.generateHelp()
// You can use your package.json to grab some useful values
const context = JSON.parse('package.json')
const customContext = myArgs.generateHelp(context)
// Or you can go further by providing your own template
const helpTemplate = `...`
const customHelp = myArgs.generateHelp(context, helpTemplate)
This is the default template :
const defaultHelpTemplate = `
{{name}} v{{version}} - {{description}} - by {{author|safe}}
Usage:
{{name}}{% for group,params in groups %} [{{ group }}]{% endfor %}
{% for group,params in groups %}
{{group|title}}s:
{% for param in params %}
{% if param.hidden != true -%}
{{ param.humanReadable|padEnd(params.padding, params.prepadding, opts.paramDescriptionSpacer)|safe }} {{param.help}}
{%- endif %}
{%- endfor %}
{% endfor -%}
`
It is pretty much self explanatory, please have a look a nunjucks for details.
Contributing
Licence is MIT, feel free to raise issues, provide PR or just drop a word about how you use it, what would be the next features you would like this piece of software to have.