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', () => {
|
describe('argparse', () => {
|
||||||
|
|
||||||
it('parses args', () => {
|
it('parses args', () => {
|
||||||
const args = argparse(['--one', '1', '--two', '2', '--four'], {
|
const args = argparse({
|
||||||
one: {
|
one: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
@ -14,8 +14,8 @@ describe('argparse', () => {
|
|||||||
},
|
},
|
||||||
four: {
|
four: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
}
|
},
|
||||||
})
|
})(['--one', '1', '--two', '2', '--four'])
|
||||||
|
|
||||||
const one: string = args.one
|
const one: string = args.one
|
||||||
const two: number = args.two
|
const two: number = args.two
|
||||||
@ -26,12 +26,120 @@ describe('argparse', () => {
|
|||||||
expect(four).toBe(true)
|
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', () => {
|
it('throws when required args missing', () => {
|
||||||
expect(() => argparse([], {
|
expect(() => argparse({
|
||||||
one: {
|
one: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
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> {
|
export interface IArgConfig<T extends TArgTypeName> {
|
||||||
type: T
|
type: T
|
||||||
alias?: string
|
alias?: string
|
||||||
|
description?: string
|
||||||
default?: TArgType<T>
|
default?: TArgType<T>
|
||||||
required?: boolean
|
required?: boolean
|
||||||
}
|
}
|
||||||
@ -19,7 +20,7 @@ export interface IArgsConfig {
|
|||||||
[arg: string]: IArgConfig<TArgTypeName>
|
[arg: string]: IArgConfig<TArgTypeName>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TArgs<T> = {
|
export type TArgs<T extends IArgsConfig> = {
|
||||||
[k in keyof T]: T[k] extends IArgConfig<infer A> ?
|
[k in keyof T]: T[k] extends IArgConfig<infer A> ?
|
||||||
TArgType<A> : never
|
TArgType<A> : never
|
||||||
}
|
}
|
||||||
@ -35,7 +36,7 @@ const iterate = <T>(arr: T[]) => {
|
|||||||
},
|
},
|
||||||
peek(): T {
|
peek(): T {
|
||||||
return arr[i + 1]
|
return arr[i + 1]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,54 +46,75 @@ function assert(cond: boolean, message: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function argparse<T extends object>(
|
function getDefaultValue(type: TArgTypeName) {
|
||||||
args: string[],
|
switch (type) {
|
||||||
config: T extends IArgsConfig ? T : never,
|
case 'number':
|
||||||
): TArgs<T> {
|
return NaN
|
||||||
const result = {} as TArgs<T>
|
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 it = iterate(args)
|
||||||
|
|
||||||
const usedArgs: Record<string, true> = {}
|
|
||||||
const aliases: Record<string, string> = {}
|
const aliases: Record<string, string> = {}
|
||||||
const requiredArgs = Object.keys(config).reduce((obj, arg) => {
|
const requiredArgs = Object.keys(config).reduce((obj, arg) => {
|
||||||
const argConfig = config[arg]
|
const argConfig = config[arg]
|
||||||
if (argConfig.default !== undefined) {
|
result[arg] = argConfig.default !== undefined
|
||||||
result[arg] = argConfig.default
|
? argConfig.default
|
||||||
}
|
: getDefaultValue(argConfig.type)
|
||||||
if (argConfig.alias) {
|
if (argConfig.alias) {
|
||||||
|
assert(
|
||||||
|
argConfig.alias in aliases === false,
|
||||||
|
'Duplicate alias: ' + argConfig.alias)
|
||||||
aliases[argConfig.alias] = arg
|
aliases[argConfig.alias] = arg
|
||||||
}
|
}
|
||||||
obj[arg] = !!argConfig.required
|
if (argConfig.required) {
|
||||||
|
obj[arg] = true
|
||||||
|
}
|
||||||
return obj
|
return obj
|
||||||
}, {} as Record<string, boolean>)
|
}, {} as Record<string, true>)
|
||||||
|
|
||||||
|
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()) {
|
while (it.hasNext()) {
|
||||||
const arg = it.next()
|
const arg = it.next()
|
||||||
assert(arg.substring(0, 1) === '-', 'Arguments must start with -')
|
assert(arg.substring(0, 1) === '-', 'Arguments must start with -')
|
||||||
let argName: string
|
const argName: string = processFlags(arg)
|
||||||
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 argConfig = config[argName]
|
const argConfig = config[argName]
|
||||||
assert(!!argConfig, 'Unknown argument: ' + arg)
|
assert(!!argConfig, 'Unknown argument: ' + arg)
|
||||||
delete requiredArgs[argName]
|
delete requiredArgs[argName]
|
||||||
usedArgs[argName] = true
|
|
||||||
const peek = it.peek()
|
const peek = it.peek()
|
||||||
switch (argConfig.type) {
|
switch (argConfig.type) {
|
||||||
case 'string':
|
case 'string':
|
||||||
@ -116,12 +138,12 @@ export function argparse<T extends object>(
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown type:' + argConfig.type)
|
assert(false, 'Unknown type: ' + argConfig.type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(!Object.keys(requiredArgs).length, 'Missing required args: ' +
|
assert(!Object.keys(requiredArgs).length, 'Missing required args: ' +
|
||||||
Object.keys(requiredArgs).map(r => '--' + r))
|
Object.keys(requiredArgs).join(', '))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user