Add more tests for argparse

This commit is contained in:
Jerko Steiner 2019-08-05 22:07:37 +07:00
parent 86e90d28bb
commit cea9bc0dd1
2 changed files with 174 additions and 44 deletions

View File

@ -1,9 +1,9 @@
import {argparse} from './argparse'
import {argparse, IArgsConfig} from './argparse'
describe('argparse', () => {
it('parses args', () => {
const args = argparse(['--one', '1', '--two', '2', '--four'], {
const args = argparse({
one: {
type: 'string',
required: true,
@ -14,8 +14,8 @@ describe('argparse', () => {
},
four: {
type: 'boolean',
}
})
},
})(['--one', '1', '--two', '2', '--four'])
const one: string = args.one
const two: number = args.two
@ -26,12 +26,120 @@ describe('argparse', () => {
expect(four).toBe(true)
})
describe('boolean', () => {
it('is not required, and is false by default', () => {
const result = argparse({
bool: {
type: 'boolean',
},
})([])
const value: boolean = result.bool
expect(value).toBe(false)
})
it('can be made required', () => {
expect(() => argparse({
bool: {
type: 'boolean',
required: true,
},
})([])).toThrowError(/Missing required args: bool/)
})
it('optionally accepts a true/false value', () => {
const parse = argparse({
bool: {
type: 'boolean',
alias: 'b',
},
other: {
type: 'string',
},
})
expect(parse(['--bool']).bool).toBe(true)
expect(parse(['--bool', 'false']).bool).toBe(false)
expect(parse(['--bool', 'true']).bool).toBe(true)
expect(parse(['--bool', '--other', 'value'])).toEqual({
bool: true,
other: 'value',
})
})
it('can be grouped by shorthand (single dash) notation', () => {
const parse = argparse({
a1: {
type: 'boolean',
alias: 'a',
},
b: {
type: 'boolean',
alias: 'c',
},
other: {
type: 'string',
alias: 'o',
},
})
expect(parse([])).toEqual({
a1: false,
b: false,
other: '',
})
expect(parse(['-ab'])).toEqual({
a1: true,
b: true,
other: '',
})
expect(parse(['-ca'])).toEqual({
a1: true,
b: true,
other: '',
})
expect(parse(['-abo', 'test'])).toEqual({
a1: true,
b: true,
other: 'test',
})
expect(() => parse(['-abo'])).toThrowError(/must be a string: -abo/)
})
})
describe('number', () => {
it('sets to NaN by default', () => {
const parse = argparse({
a: {
type: 'number',
},
})
expect(parse([])).toEqual({
a: NaN,
})
expect(() => parse(['-a'])).toThrowError(/must be a number: -a/)
expect(() => parse(['-a', 'no-number']))
.toThrowError(/must be a number: -a/)
expect(() => parse(['--a', 'no-number']))
.toThrowError(/must be a number: --a/)
expect(parse(['-a', '10'])).toEqual({
a: 10,
})
expect(parse(['--a', '11'])).toEqual({
a: 11,
})
})
})
it('throws when required args missing', () => {
expect(() => argparse([], {
expect(() => argparse({
one: {
type: 'string',
required: true,
},
})).toThrowError(/missing required/i)
})([])).toThrowError(/missing required/i)
})
it('throws when arg type is unknown', () => {
expect(() => argparse({
a: {
type: 'test',
} as any,
})(['-a'])).toThrowError(/Unknown type: test/)
})
})

View File

@ -11,6 +11,7 @@ export type TArgType<T extends TArgTypeName> =
export interface IArgConfig<T extends TArgTypeName> {
type: T
alias?: string
description?: string
default?: TArgType<T>
required?: boolean
}
@ -19,7 +20,7 @@ export interface IArgsConfig {
[arg: string]: IArgConfig<TArgTypeName>
}
export type TArgs<T> = {
export type TArgs<T extends IArgsConfig> = {
[k in keyof T]: T[k] extends IArgConfig<infer A> ?
TArgType<A> : never
}
@ -35,7 +36,7 @@ const iterate = <T>(arr: T[]) => {
},
peek(): T {
return arr[i + 1]
}
},
}
}
@ -45,56 +46,77 @@ function assert(cond: boolean, message: string) {
}
}
export function argparse<T extends object>(
args: string[],
config: T extends IArgsConfig ? T : never,
): TArgs<T> {
const result = {} as TArgs<T>
function getDefaultValue(type: TArgTypeName) {
switch (type) {
case 'number':
return NaN
case 'string':
return ''
case 'boolean':
return false
}
}
export const argparse = <T extends IArgsConfig>(
config: T,
) => (args: string[]): TArgs<T> => {
const result: any = {}
const it = iterate(args)
const usedArgs: Record<string, true> = {}
const aliases: Record<string, string> = {}
const requiredArgs = Object.keys(config).reduce((obj, arg) => {
const argConfig = config[arg]
if (argConfig.default !== undefined) {
result[arg] = argConfig.default
}
result[arg] = argConfig.default !== undefined
? argConfig.default
: getDefaultValue(argConfig.type)
if (argConfig.alias) {
assert(
argConfig.alias in aliases === false,
'Duplicate alias: ' + argConfig.alias)
aliases[argConfig.alias] = arg
}
obj[arg] = !!argConfig.required
if (argConfig.required) {
obj[arg] = true
}
return obj
}, {} as Record<string, boolean>)
}, {} as Record<string, true>)
while(it.hasNext()) {
function getArgumentName(nameOrAlias: string): string {
return nameOrAlias in config ? nameOrAlias : aliases[nameOrAlias]
}
function processFlags(arg: string): string {
if (arg.substring(1, 2) === '-') {
return arg.substring(2)
}
const flags = arg.substring(1).split('')
flags.slice(0, flags.length - 1)
.forEach(flag => {
const argName = getArgumentName(flag)
const argConfig = config[argName]
assert(!!argConfig, 'Unknown argument: ' + flag)
assert(argConfig.type === 'boolean',
'The argument is not a flag/boolean: ' + flag)
delete requiredArgs[argName]
result[argName] = true
})
const lastArgName = getArgumentName(flags[flags.length - 1])
assert(!!lastArgName, 'Unknown argument: ' + lastArgName)
return lastArgName
}
while (it.hasNext()) {
const arg = it.next()
assert(arg.substring(0, 1) === '-', 'Arguments must start with -')
let argName: string
if (arg.substring(1, 2) !== '-') {
// flags
const flags = arg.substring(1).split('')
flags.slice(0, flags.length - 2)
.forEach(flag => {
const alias = aliases[flag]
const argConfig = config[alias]
assert(!!argConfig, 'Unknown flag: ' + flag)
assert(argConfig.type === 'boolean', 'The argument is not a flag/boolean: ' + flag)
delete requiredArgs[alias]
result[alias] = true
})
const lastArg = flags[flags.length - 1]
argName = aliases[lastArg]
} else {
argName = arg.substring(2)
}
const argName: string = processFlags(arg)
const argConfig = config[argName]
assert(!!argConfig, 'Unknown argument: ' + arg)
delete requiredArgs[argName]
usedArgs[argName] = true
const peek = it.peek()
switch(argConfig.type) {
switch (argConfig.type) {
case 'string':
assert(it.hasNext(), 'Value of argument must be a string: ' + arg)
result[argName] = it.next()
@ -116,12 +138,12 @@ export function argparse<T extends object>(
}
continue
default:
throw new Error('Unknown type:' + argConfig.type)
assert(false, 'Unknown type: ' + argConfig.type)
}
}
assert(!Object.keys(requiredArgs).length, 'Missing required args: ' +
Object.keys(requiredArgs).map(r => '--' + r))
Object.keys(requiredArgs).join(', '))
return result
}