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', () => { describe('positional', () => {
it('can be defined', () => { it('can be defined', () => {
const {parse} = argparse({ 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> = export type TArgType<T extends TArgTypeName> =
T extends 'string' T extends 'string'
? string ? string
: T extends 'string[]'
? string[]
: T extends 'number' : T extends 'number'
? number ? number
: T extends 'boolean' : T extends 'boolean'
? boolean ? boolean
: never : 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 let exit = () => process.exit()
export interface IArgParam<T extends TArgTypeName> { export interface IArgParam<T extends TArgTypeName> {
@ -17,18 +25,19 @@ export interface IArgParam<T extends TArgTypeName> {
choices?: Array<TArgType<T>> choices?: Array<TArgType<T>>
required?: boolean required?: boolean
positional?: 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 type: T
} }
export interface IArgsConfig { export interface IArgsConfig {
[arg: string]: IArgConfig<TArgTypeName> [arg: string]: IArgument<TArgTypeName>
} }
export type TArgs<T extends IArgsConfig> = { 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 TArgType<A> : never
} }
@ -71,6 +80,8 @@ function getDefaultValue(type: TArgTypeName) {
return '' return ''
case 'boolean': case 'boolean':
return false return false
case 'string[]':
return [] as string[]
} }
} }
@ -108,6 +119,29 @@ function getValue(
return isPositional ? argument : it.next() 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[]) { export function isHelp(argv: string[]) {
return argv.some(a => /^(-h|--help)$/.test(a)) return argv.some(a => /^(-h|--help)$/.test(a))
} }
@ -130,14 +164,35 @@ export function padRight(str: string, chars: number) {
export function help(config: IArgsConfig) { export function help(config: IArgsConfig) {
const keys = Object.keys(config) 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 = [ const positionalHelp = [
'[OPTIONS]', '[OPTIONS]',
keys keys
.filter(k => config[k].positional) .filter(k => config[k].positional)
.map(k => config[k].required .map(k => getArrayHelp(k, config[k].required, config[k].n))
? `${k.toUpperCase()}`
: `[${k.toUpperCase()}]`,
)
.join(' '), .join(' '),
].join(' ') ].join(' ')
@ -160,7 +215,10 @@ export function help(config: IArgsConfig) {
const description = argConfig.description const description = argConfig.description
? ' ' + argConfig.description : '' ? ' ' + argConfig.description : ''
const sample = samples.length ? ` (${samples.join(', ')})` : '' 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') .join('\n')
@ -172,7 +230,7 @@ export function help(config: IArgsConfig) {
export function arg<T extends TArgTypeName>( export function arg<T extends TArgTypeName>(
type: T, type: T,
config: IArgParam<T> = {}, config: IArgParam<T> = {},
): IArgConfig<T> { ): IArgument<T> {
return { return {
...config, ...config,
type, type,
@ -245,9 +303,14 @@ export function argparse<T extends IArgsConfig>(config: T) {
return p! return p!
} }
let onlyPositionals = false
while (it.hasNext()) { while (it.hasNext()) {
const argument = it.next() 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 const argName = !isPositional
? processFlags(argument) ? processFlags(argument)
: getNextPositional() : getNextPositional()
@ -260,6 +323,10 @@ export function argparse<T extends IArgsConfig>(config: T) {
assert(!!result[argName], assert(!!result[argName],
'Value of argument must be a string: ' + argument) 'Value of argument must be a string: ' + argument)
break break
case 'string[]':
result[argName] = extractArray(
it, argument, isPositional, argConfig.n)
break
case 'number': case 'number':
const num = parseInt(getValue(it, argument, isPositional), 10) const num = parseInt(getValue(it, argument, isPositional), 10)
assert(!isNaN(num), assert(!isNaN(num),