Add more tests for argparse
This commit is contained in:
parent
86e90d28bb
commit
cea9bc0dd1
@ -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/)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user