argparse: Add support for string[], number of args
This commit is contained in:
parent
78f39517ce
commit
7182684b4d
@ -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({
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user