argv-reader
Primitive commandline option processing library.
Concept
- Prefer combination of libraries to one library covering all
- Focus on providing a building block of commandline option processing to implement the bahavior that developers hope to realize
Getting Started
Import
import { ArgvReader } from 'argv-reader'
Parse flags
type RawOpts = {
flags: {
flag1?: boolean,
},
}
const reader = new ArgvReader<unknown, RawOpts, RawOpts>(
arg => {
if (arg.startsWith('-')) {
switch (arg) {
case '-f': case '--flag1':
return ['flag', 'flag1']
}
}
return false
},
opts => opts,
)
const opts = reader.read(['-f'])
console.log(opts)
// -> { flags: { flag1: true } }
- Construct a reader by passing two arguments:
- The first (
arg => ...
) is an extractor which receives each element and determines how to treat it. - The second (
opts => ...
) is a converter which receives the result from the reader and converts it to the firnal result.
- The first (
-
reader.read()
reads elements from passed array one by one. - The reader passes each element to the user supplied extractor function (
arg => ...
) . - The extractor determines if the passed element is a flag, and if so, returns
['flag', 'flag1']
.- The first return value
'flag'
indicates how to treat the passed element. in this case, treat it as a flag. - The second return value
'flag1'
indicates the name of a flag.
- The first return value
- The extractor returns
false
if the passed element is not a flag. This indicates it is interpreted as part of rest arguments (as seen later.) - The reader interprets returned value from extractor and stores the result.
- flags are stored in
flags.{{flag name}}
.
- flags are stored in
- After all elements are read, the reader passes the result to the user supplied converter function (
opts => ...
), which just returns the same as the final result here. -
reader.read()
returns the final result.
Parse options which takes its argument
type RawOpts = {
singles: {
single1?: string,
},
}
const reader = new ArgvReader<unknown, RawOpts, RawOpts>(
arg => {
if (arg.startsWith('-')) {
switch (arg) {
case '-s': case '--single':
return ['single', 'single1']
}
}
return false
},
opts => opts,
)
const opts = reader.read(['-s', 'a-value'])
console.log(opts)
// -> { singles: { single1: 'a-value' } }
- The extractor determines if the passed element is an option with a value, and if so, returns
['single', 'single1']
-
'single'
at the first position means to interpret it as a option with a value. -
'single1'
at the second position is the name of the option.
-
- After receiving the instruction from the extractor, the reader reads the next element (which is not passed to the extractor) and stores it as the value of the option.
- The reader stores option values in
singles.{{option name}}
- The reader stores option values in
Parse rest arguments
type RawOpts = {
rest: string[],
}
const reader = new ArgvReader<unknown, RawOpts, RawOpts>(
arg => {
return false
},
opts => opts,
)
const opts = reader.read(['a', 'b'])
console.log(opts)
// -> { rest: ['a', 'b'] }
- If the extractor returns
false
, the reader stores the read element inrest
array.
Put them together
type RawOpts = {
flags: {
flag1?: boolean,
},
singles: {
single1?: string,
},
rest: string[],
}
const reader = new ArgvReader<unknown, RawOpts, RawOpts>(
arg => {
if (arg.startsWith('-')) {
switch (arg) {
case '-f': case '--flag1':
return ['flag', 'flag1']
case '-s': case '--single':
return ['single', 'single1']
}
}
return false
},
opts => opts,
)
const opts = reader.read(['-f', '-s', 'a-value', 'a', 'b'])
console.log(opts)
// -> {
// flags: { flag1: true },
// singles: { single1: 'a-value' },
// rest: ['a', 'b'],
// }
If unknown options are given
-
How to do is dependent on expected behavior.
-
If they should be treated as rest arguments, the above extractor is satisfactory.
-
If error should be thrown,
type RawOpts = { flags: { flag1?: boolean, }, singles: { single1?: string, }, rest: string[], } const reader = new ArgvReader<unknown, RawOpts, RawOpts>( arg => { if (arg.startsWith('-')) { switch (arg) { case '-f': case '--flag1': return ['flag', 'flag1'] case '-s': case '--single': return ['single', 'single1'] } // added the following line throw new Error(`unknown option: ${arg}`) } return false }, opts => opts, ) const opts = reader.read(['-h']) // -> Error: unknown option: -h
Treat all arguments after '--' as rest arguments
-
The extractor can return
'rest'
to show the elements after this marker are treated as rest arguments, which are not passed to the extractor any more.type RawOpts = { flags: { flag1?: boolean, }, singles: { single1?: string, }, rest: string[], } const reader = new ArgvReader<unknown, RawOpts, RawOpts>( arg => { if (arg.startsWith('-')) { // added the following lines if (arg === '--') { return 'rest' } switch (arg) { case '-f': case '--flag1': return ['flag', 'flag1'] case '-s': case '--single': return ['single', 'single1'] } throw new Error(`unknown option: ${arg}`) } return false }, opts => opts, ) const opts = reader.read(['--', '-h']) console.log(opts) // -> { // rest: ['-h'], // ... // }
Treat concatenated short options as each separated option
-
The extractor can return
'replace'
to replace the current element with alternative elements.type RawOpts = { flags: { flag1?: boolean, }, // added the following lines multiflags: { verbose?: number, }, singles: { single1?: string, }, rest: string[], } const reader = new ArgvReader<unknown, RawOpts, RawOpts>( arg => { if (arg.startsWith('-')) { if (arg === '--') { return 'rest' } switch (arg) { case '-f': case '--flag1': return ['flag', 'flag1'] case '-s': case '--single': return ['single', 'single1'] // added the following lines case '-v': return ['multiflag', 'verbose'] } // added the following lines if (/^-vv+$/.test(arg)) { const alt = arg.split('').slice(1).map(a => `-${a}`) return ['replace', alt] } throw new Error(`unknown option: ${arg}`) } return false }, opts => opts, ) const opts = reader.read(['-vvv']) console.log(opts) // -> { // multiflags: { verbose: 3 }, // ... // }
-
/^-vv+$/.test(arg)
tests if the current element is like'-vvv'
. -
['replace', alt]
means to replace the current element with the sequence of elements thatalt
represents, and call the extractor again for the current element. So['-vvv']
becomes['-v', '-v', '-v']
. -
multiflag
counts appearance of a flag instead stores a flag value. The result is stored inmultiflags.{{flag name}}
.
-
Treat an option that optionally takes an argument
-
The extractor can look ahead the next element and determine if the current element should take the next element as its argument, or should not take the next element and take default value as its argument.
type RawOpts = { flags: { flag1?: boolean, }, singles: { single1?: string, }, rest: string[], } const reader = new ArgvReader<unknown, RawOpts, RawOpts>( arg => { if (arg.startsWith('-')) { if (arg === '--') { return 'rest' } switch (arg) { case '-f': case '--flag1': return ['flag', 'flag1'] case '-s': case '--single': // changed the following lines return ['lookahead', la => la == null || la.startsWith('-') ? ['replace', [arg, '']] : ['single', 'single1']] } throw new Error(`unknown option: ${arg}`) } return false }, opts => opts, ) const opts = reader.read(['-s', '-f']) console.log(opts) // -> { // flags: { flag1: true }, // singles: { single1: '' }, // ... // } // , instead of // { // flags: {}, // singles: { single1: '-f' }, // ... // }
-
['lookahead', f]
instructs the reader to pass the next element to f, which returns the determined instruction for the current element to the reader. -
['replace', [arg, '']]
means to replace the current element with the sequence ofarg
and''
, and call the extractor again for the current element. So['-s', '-f']
becomes['-s', '', '-f']
.
-
Implement sub commands
-
The implementation plan is as follows:
- Implement a top level commandline option parser.
This stores the result of type{ command: string, rest: string[] }
wherecommand
represents the name of sub command andrest
represents rest arguments which are fed to each sub command as its own arguments. - Implement commandline option parsers for each sub command.
This parses rest arguments of the top level parser.
- Implement a top level commandline option parser.
-
The top level parsr would be like this:
type ParsingTopOpts = { arguments: { command?: string[] } rest: string[] } const reader = new ArgvReader<'rest', ParsingTopOpts, ParsingTopOpts>( (arg, state) => { if (state === 'rest') { return ['argument', 'rest'] } if (arg.startsWith('-')) { if (arg === '--') { return 'rest' } throw new Error(`unknown option: ${arg}`) } if (state == null) { return ['argument', 'command', 'rest'] } return false }, opts => opts ) const topOpts = reader.read(process.argv.slice(2))
- The extractor recieves
state
as the second argument.- It is of type expressed by the first type parameter of
ArgvReader
. Here,'rest'
(or exactly,'rest' | undefined
). - It has
undefined
as an initial value.
- It is of type expressed by the first type parameter of
- The extractor can update
state
by returing new state value.- The new state value is filled in the extra trailing element of the returned value array. Here,
'rest'
of['argument', 'command', 'rest']
.
- The new state value is filled in the extra trailing element of the returned value array. Here,
-
['argument', 'rest']
means to treat the current element as the argument named'rest'
.- At the second position is the name of the argument.
- The reader stores the value of the named argument in
arguments.{{argument name}}
array of the result object. - Howver, the argument named
'rest'
is treated specially and stored inrest
array instead ofarguments.rest
for the purpose of compatibility with normal rest arguments.
-
['argument', 'command', 'rest']
means to treat the current element as the argument named'command'
which is stored inarguments.command
, and update the state to'rest'
.- At the third position is the new state value desribed above.
- In sum:
- Until the extractor sees the first argument except for flags,
state
remainsnull
and it parses flags normally. - If the extractor encounters an element that is not a flag for the first time, it treats that element as the argument named
'command'
, and update the state to'rest'
. - The extractor continues to run in the state of
'rest'
and treats all remaining arguments as rest arguments.
- Until the extractor sees the first argument except for flags,
- The extractor recieves
-
Then dispatch rest arguments to sub commands:
if (topOpts.arguments.command == null || topOpts.arguments.command.length == 0) { throw Error('command is mandatory') } switch (topOpts.arguments.command[0]) { case 'command1': { const subreader = new ArgvReader // ... const subOpts = subreader.read(topOpts.rest) doSomethingOfSubCommmand1(topOpts, subOpts) break } // ... }
Return structs instead of tuples
-
From v1.2.0, extractors can return structs instead of tuples. The former is more readable in some cases.
For example:return { type: 'rest' } return { type: 'rest', state: 'state1' } return { type: 'skip' } return { type: 'skip', state: 'state1' } return { type: 'flag', name: 'flag1' } return { type: 'flag', name: 'flag1', state: 'state1' } return { type: 'multiflag', name: 'multiflag1' } return { type: 'multiflag', name: 'multiflag1', state: 'state1' } return { type: 'noflag', name: 'flag1' } return { type: 'noflag', name: 'flag1', state: 'state1' } return { type: 'single', name: 'single1' } return { type: 'single', name: 'single1', state: 'state1' } return { type: 'multiple', name: 'multiple1' } return { type: 'multiple', name: 'multiple1', state: 'state1' } return { type: 'argument', name: 'argument1' } return { type: 'argument', name: 'argument1', state: 'state1' } return { type: 'replace', replace: ['replacer'] } return { type: 'replace', replace: ['replacer'], state: 'state1' } return { type: 'lookahead', lookahead: la => la == null || la.startsWith('-') ? { type: 'replace', replace: [arg, ''] } : { type: 'single', name: 'optional1' } }
What this library does not provide
Check if invalid values are given to options
- You can check if options have expected values by yourself.
- It is not in the scope of this library.
It can be realized by combination of other libraries, which seems more flexible and preferrable.
Show help
-
You can write it by yourself independently from the commandline processing library:
const help = `\ usage: [options] [<arguments>] options: -f|--flag1 explanation of flag1 -s|--single1 <single1> explanation of single1 arguments: explanation of arguments `
-
It is possible to provide separate libraries to generate help text from declared options information.
It seems more flexible and preferrable to users being enforced specific format by the commandline option processing library that they chose accidentally.
Need declarative interface
- You can construct it by yourself on top of the commandline processing library.
- for example, declare options information like:
const options = { flags: { flag1: { short: '-f' long: '--flag1' } } }
- then implement an extractor function using it:
arg => { if (arg.startsWith('-')) { for (const optName of Object.keys(options.flags)) { if (arg === options.flags[optName].short || arg === options.flags[optName].long) { return ['flag', optName] } } // ... } // ... }
- for example, declare options information like:
- Again, it is possible to provide separate libraries like this and it seems more flexible and preferrable.
Example
- More examples can be found in
examples
directory of the repository. - Test cases in
tests
directory may also be useful.
License
This library is licensed under the MIT License.