Add support for positional args in @rondo/argparse

This commit is contained in:
Jerko Steiner 2019-08-05 22:56:40 +07:00
parent 7ea1c34428
commit b50dfb1455
2 changed files with 102 additions and 5 deletions

View File

@ -125,6 +125,76 @@ describe('argparse', () => {
})
})
describe('positional', () => {
it('can be defined', () => {
const parse = argparse({
a: {
type: 'number',
positional: true,
},
})
expect(parse([]).a).toBe(NaN)
expect(parse(['12']).a).toBe(12)
})
it('works with booleans', () => {
const parse = argparse({
a: {
type: 'boolean',
positional: true,
},
})
expect(parse([]).a).toBe(false)
expect(parse(['true']).a).toBe(true)
expect(parse(['false']).a).toBe(false)
expect(() => parse(['invalid'])).toThrowError(/true or false/)
})
it('works with strings', () => {
const parse = argparse({
a: {
type: 'string',
positional: true,
},
})
expect(parse([]).a).toBe('')
expect(parse(['a']).a).toBe('a')
})
it('works with multiple positionals', () => {
const parse = argparse({
a: {
type: 'string',
positional: true,
},
b: {
type: 'string',
positional: true,
},
})
expect(parse(['aaa', 'bbb'])).toEqual({
a: 'aaa',
b: 'bbb',
})
})
it('works amongs regular arguments', () => {
const parse = argparse({
arg1: {
type: 'string',
},
arg2: {
type: 'number',
positional: true,
},
arg3: {
type: 'string',
},
})
expect(parse(['--arg1', 'one', '2', '--arg3', 'three'])).toEqual({
arg1: 'one',
arg2: 2,
arg3: 'three',
})
})
})
it('throws when required args missing', () => {
expect(() => argparse({
one: {

View File

@ -14,6 +14,7 @@ export interface IArgConfig<T extends TArgTypeName> {
description?: string
default?: TArgType<T>
required?: boolean
positional?: boolean
}
export interface IArgsConfig {
@ -64,6 +65,7 @@ export const argparse = <T extends IArgsConfig>(
const it = iterate(args)
const aliases: Record<string, string> = {}
const positional: string[] = []
const requiredArgs = Object.keys(config).reduce((obj, arg) => {
const argConfig = config[arg]
result[arg] = argConfig.default !== undefined
@ -75,6 +77,9 @@ export const argparse = <T extends IArgsConfig>(
'Duplicate alias: ' + argConfig.alias)
aliases[argConfig.alias] = arg
}
if (argConfig.positional) {
positional.push(arg)
}
if (argConfig.required) {
obj[arg] = true
}
@ -108,25 +113,47 @@ export const argparse = <T extends IArgsConfig>(
return lastArgName
}
function getNextPositional(): string {
const p = positional.shift()
assert(!!p, 'No defined positional arguments')
return p!
}
while (it.hasNext()) {
const arg = it.next()
assert(arg.substring(0, 1) === '-', 'Arguments must start with -')
const argName: string = processFlags(arg)
const isPositional = arg.substring(0, 1) !== '-'
const argName = !isPositional
? processFlags(arg)
: getNextPositional()
const argConfig = config[argName]
assert(!!argConfig, 'Unknown argument: ' + arg)
delete requiredArgs[argName]
const peek = it.peek()
switch (argConfig.type) {
case 'string':
assert(it.hasNext(), 'Value of argument must be a string: ' + arg)
result[argName] = it.next()
if (isPositional) {
result[argName] = arg
} else {
assert(it.hasNext(), 'Value of argument must be a string: ' + arg)
result[argName] = it.next()
}
continue
case 'number':
const num = parseInt(it.next(), 10)
const num = parseInt(isPositional ? arg : it.next(), 10)
assert(!isNaN(num), 'Value of argument must be a number: ' + arg)
result[argName] = num
continue
case 'boolean':
if (isPositional) {
if (arg === 'true') {
result[argName] = true
} else if (arg === 'false') {
result[argName] = false
} else {
assert(false, 'Value of argument must be true or false: ' + arg)
}
continue
}
if (peek === 'true') {
it.next()
result[argName] = true