argparse: Add support for string[], number of args

This commit is contained in:
Jerko Steiner 2019-08-13 19:37:15 +07:00
parent 78f39517ce
commit 7182684b4d
2 changed files with 170 additions and 11 deletions

View File

@ -142,6 +142,98 @@ describe('argparse', () => {
})
})
describe('string[] and n', () => {
it('has a value of n = 1 by default', () => {
const {parse, help} = argparse({
value: {
type: 'string[]',
},
})
expect(parse([]).value).toEqual([])
expect(parse(['--value', 'one']).value).toEqual(['one'])
expect(help()).toEqual([
'[OPTIONS] ',
'',
'Options:',
' --value [VALUE] ',
].join('\n'))
})
it('can be used to extract finite number of values', () => {
const {parse, help} = argparse({
value: {
type: 'string[]',
n: 3,
},
other: {
type: 'number',
alias: 'o',
},
})
expect(parse([]).value).toEqual([])
expect(parse(['--value', 'a', 'b', '--other', '-o', '3'])).toEqual({
value: ['a', 'b', '--other'],
other: 3,
})
expect(help()).toEqual([
'[OPTIONS] ',
'',
'Options:',
' --value [VALUE1 VALUE2 VALUE3] ',
'-o, --other number ',
].join('\n'))
})
it('can be used to collect any remaining arguments when n = "+"', () => {
const {parse, help} = argparse({
value: arg('string[]', {n: '+', required: true}),
other: arg('number'),
})
expect(() => parse([])).toThrowError(/Missing required args: value/)
expect(parse(['--value', 'a', '--other', '3'])).toEqual({
value: ['a', '--other', '3'],
other: NaN,
})
expect(parse(['--other', '2', '--value', 'a', '--other', '3'])).toEqual({
value: ['a', '--other', '3'],
other: 2,
})
expect(help()).toEqual([
'[OPTIONS] ',
'',
'Options:',
' --value VALUE... (required)',
' --other number ',
].join('\n'))
})
it('can collect remaining positional arguments when n = "*"', () => {
const {parse, help} = argparse({
value: arg('string[]', {n: '*', required: true, positional: true}),
other: arg('number'),
})
expect(parse(['a', 'b']).value).toEqual(['a', 'b'])
expect(() => parse(['--other', '3']).value)
.toThrowError(/Missing.*: value/)
expect(parse(['--other', '2', '--', '--other', '3'])).toEqual({
value: ['--other', '3'],
other: 2,
})
expect(parse(['--', '--other', '3'])).toEqual({
value: ['--other', '3'],
other: NaN,
})
expect(parse(['--other', '3', 'a', 'b', 'c'])).toEqual({
value: ['a', 'b', 'c'],
other: 3,
})
expect(help()).toEqual([
'[OPTIONS] [VALUE...]',
'',
'Options:',
' --value [VALUE...] (required)',
' --other number ',
].join('\n'))
})
})
describe('positional', () => {
it('can be defined', () => {
const {parse} = argparse({

View File

@ -1,13 +1,21 @@
export type TArgTypeName = 'string' | 'number' | 'boolean'
export type TArgTypeName = 'string' | 'string[]' | 'number' | 'boolean'
export type TArgType<T extends TArgTypeName> =
T extends 'string'
? string
: T extends 'string[]'
? string[]
: T extends 'number'
? number
: T extends 'boolean'
? boolean
: never
export const N_ONE_OR_MORE = '+'
export const N_ZERO_OR_MORE = '*'
export const N_DEFAULT_VALUE = 1
export type TNumberOfArgs = number | '+' | '*'
export let exit = () => process.exit()
export interface IArgParam<T extends TArgTypeName> {
@ -17,18 +25,19 @@ export interface IArgParam<T extends TArgTypeName> {
choices?: Array<TArgType<T>>
required?: boolean
positional?: boolean
n?: TNumberOfArgs
}
export interface IArgConfig<T extends TArgTypeName> extends IArgParam<T> {
export interface IArgument<T extends TArgTypeName> extends IArgParam<T> {
type: T
}
export interface IArgsConfig {
[arg: string]: IArgConfig<TArgTypeName>
[arg: string]: IArgument<TArgTypeName>
}
export type TArgs<T extends IArgsConfig> = {
[k in keyof T]: T[k] extends IArgConfig<infer A> ?
[k in keyof T]: T[k] extends IArgument<infer A> ?
TArgType<A> : never
}
@ -71,6 +80,8 @@ function getDefaultValue(type: TArgTypeName) {
return ''
case 'boolean':
return false
case 'string[]':
return [] as string[]
}
}
@ -108,6 +119,29 @@ function getValue(
return isPositional ? argument : it.next()
}
function extractArray(
it: IIterator<string>,
argument: string,
isPositional: boolean,
n: TNumberOfArgs = N_DEFAULT_VALUE,
): string[] {
function getLimit() {
const l = typeof n === 'number' ? n : Infinity
return isPositional ? l - 1 : l
}
const limit = getLimit()
const array = isPositional ? [argument] : []
let i = 0
for (; i < limit && it.hasNext(); i++) {
array.push(it.next())
}
if (typeof n === 'number') {
assert(i === limit,
`Expected ${limit} arguments for ${argument}, but got ${i}`)
}
return array
}
export function isHelp(argv: string[]) {
return argv.some(a => /^(-h|--help)$/.test(a))
}
@ -130,14 +164,35 @@ export function padRight(str: string, chars: number) {
export function help(config: IArgsConfig) {
const keys = Object.keys(config)
function getArrayHelp(
k: string,
required?: boolean,
n: TNumberOfArgs = N_DEFAULT_VALUE,
) {
k = k.toUpperCase()
if (n === N_ZERO_OR_MORE) {
return `[${k}...]`
}
if (n === N_ONE_OR_MORE) {
return required ? `${k}...` : `[${k}...]`
}
if (n === 1) {
return required ? k : `[${k}]`
}
const limit: number = n
const array = []
for (let i = 0; i < limit; i++) {
array.push(k + (i + 1))
}
return required ? array.join(' ') : `[${array.join(' ')}]`
}
const positionalHelp = [
'[OPTIONS]',
keys
.filter(k => config[k].positional)
.map(k => config[k].required
? `${k.toUpperCase()}`
: `[${k.toUpperCase()}]`,
)
.map(k => getArrayHelp(k, config[k].required, config[k].n))
.join(' '),
].join(' ')
@ -160,7 +215,10 @@ export function help(config: IArgsConfig) {
const description = argConfig.description
? ' ' + argConfig.description : ''
const sample = samples.length ? ` (${samples.join(', ')})` : ''
return padRight(name + ' ' + type, 30) + ' ' + description + sample
const argType = type === 'string[]'
? getArrayHelp(argument, argConfig.required, argConfig.n)
: type
return padRight(name + ' ' + argType, 30) + ' ' + description + sample
})
.join('\n')
@ -172,7 +230,7 @@ export function help(config: IArgsConfig) {
export function arg<T extends TArgTypeName>(
type: T,
config: IArgParam<T> = {},
): IArgConfig<T> {
): IArgument<T> {
return {
...config,
type,
@ -245,9 +303,14 @@ export function argparse<T extends IArgsConfig>(config: T) {
return p!
}
let onlyPositionals = false
while (it.hasNext()) {
const argument = it.next()
const isPositional = argument.substring(0, 1) !== '-'
if (argument === '--' && !onlyPositionals) {
onlyPositionals = true
continue
}
const isPositional = argument.substring(0, 1) !== '-' || onlyPositionals
const argName = !isPositional
? processFlags(argument)
: getNextPositional()
@ -260,6 +323,10 @@ export function argparse<T extends IArgsConfig>(config: T) {
assert(!!result[argName],
'Value of argument must be a string: ' + argument)
break
case 'string[]':
result[argName] = extractArray(
it, argument, isPositional, argConfig.n)
break
case 'number':
const num = parseInt(getValue(it, argument, isPositional), 10)
assert(!isNaN(num),