Add help function

This commit is contained in:
Jerko Steiner 2019-08-06 11:02:02 +07:00
parent b50dfb1455
commit 8c399c1903
2 changed files with 147 additions and 57 deletions

View File

@ -1,17 +1,11 @@
import {argparse, IArgsConfig} from './argparse' import {argparse, arg, IArgsConfig} from './argparse'
describe('argparse', () => { describe('argparse', () => {
it('parses args', () => { it('parses args', () => {
const args = argparse({ const args = argparse({
one: { one: arg('string', {required: true}),
type: 'string', two: arg('number', {default: 100}),
required: true,
},
two: {
type: 'number',
default: 1,
},
four: { four: {
type: 'boolean', type: 'boolean',
}, },
@ -125,6 +119,29 @@ describe('argparse', () => {
}) })
}) })
describe('choices', () => {
it('can enforce typed choices', () => {
const parse = argparse({
choice: arg('string', {
choices: ['a', 'b'],
}),
num: arg('number', {
choices: [1, 2],
}),
})
expect(() => parse(['--choice', 'c'])).toThrowError(/one of: a, b$/)
expect(() => parse(['--num', '3'])).toThrowError(/must be one of: 1, 2$/)
expect(parse(['--choice', 'a', '--num', '1'])).toEqual({
choice: 'a',
num: 1,
})
expect(parse(['--choice', 'b', '--num', '2'])).toEqual({
choice: 'b',
num: 2,
})
})
})
describe('positional', () => { describe('positional', () => {
it('can be defined', () => { it('can be defined', () => {
const parse = argparse({ const parse = argparse({

View File

@ -8,15 +8,19 @@ export type TArgType<T extends TArgTypeName> =
? boolean ? boolean
: never : never
export interface IArgConfig<T extends TArgTypeName> { export interface IArgParam<T extends TArgTypeName> {
type: T
alias?: string alias?: string
description?: string description?: string
default?: TArgType<T> default?: TArgType<T>
choices?: Array<TArgType<T>>
required?: boolean required?: boolean
positional?: boolean positional?: boolean
} }
export interface IArgConfig<T extends TArgTypeName> extends IArgParam<T> {
type: T
}
export interface IArgsConfig { export interface IArgsConfig {
[arg: string]: IArgConfig<TArgTypeName> [arg: string]: IArgConfig<TArgTypeName>
} }
@ -26,7 +30,13 @@ export type TArgs<T extends IArgsConfig> = {
TArgType<A> : never TArgType<A> : never
} }
const iterate = <T>(arr: T[]) => { interface IIterator<T> {
hasNext(): boolean
next(): T
peek(): T
}
const iterate = <T>(arr: T[]): IIterator<T> => {
let i = -1 let i = -1
return { return {
hasNext() { hasNext() {
@ -58,6 +68,88 @@ function getDefaultValue(type: TArgTypeName) {
} }
} }
function getBooleanValue(
it: IIterator<string>,
argument: string,
isPositional: boolean,
): boolean {
if (isPositional) {
if (argument === 'true') {
return true
} else if (argument === 'false') {
return false
} else {
throw new Error('Value of argument must be true or false: ' + arg)
}
}
const peek = it.peek()
if (peek === 'true') {
it.next()
return true
} else if (peek === 'false') {
it.next()
return false
} else {
return true
}
}
function getValue(
it: IIterator<string>,
argument: string,
isPositional: boolean,
): string {
return isPositional ? argument : it.next()
}
function checkChoice<T>(argument: string, choice: T, choices?: T[]) {
if (choices) {
assert(
choices.some(c => choice === c),
`Argument "${argument}" must be one of: ${choices.join(', ')}`)
}
}
export function padRight(str: string, chars: number) {
while (str.length < chars) {
str += ' '
}
return str
}
export function help(config: IArgsConfig) {
return Object.keys(config).map(argument => {
const argConfig = config[argument]
const {alias, description, type} = argConfig
const name = alias
? `-${alias}, --${argument}`
: ` --${argument}`
const samples = []
if (argConfig.required) {
samples.push('required')
}
if (argConfig.default) {
samples.push('default: ' + argConfig.default)
}
if (argConfig.choices) {
samples.push('choices: ' + argConfig.choices)
}
const sample = samples.length ? ` (${samples.join(', ')})` : ''
return padRight(name + ' ' + type, 20) + ' ' + description + sample
})
.join('\n')
}
export function arg<T extends TArgTypeName>(
type: T,
config: IArgParam<T> = {},
): IArgConfig<T> {
return {
...config,
type,
}
}
export const argparse = <T extends IArgsConfig>( export const argparse = <T extends IArgsConfig>(
config: T, config: T,
) => (args: string[]): TArgs<T> => { ) => (args: string[]): TArgs<T> => {
@ -66,22 +158,24 @@ export const argparse = <T extends IArgsConfig>(
const aliases: Record<string, string> = {} const aliases: Record<string, string> = {}
const positional: string[] = [] const positional: string[] = []
const requiredArgs = Object.keys(config).reduce((obj, arg) => { const requiredArgs = Object.keys(config).reduce((obj, argument) => {
const argConfig = config[arg] const argConfig = config[argument]
result[arg] = argConfig.default !== undefined result[argument] = argConfig.default !== undefined
? argConfig.default ? argConfig.default
: getDefaultValue(argConfig.type) : getDefaultValue(argConfig.type)
if (argConfig.alias) { if (argConfig.alias) {
assert(argConfig.alias.length === 1,
'Alias must be a single character: ' + argConfig.alias)
assert( assert(
argConfig.alias in aliases === false, argConfig.alias in aliases === false,
'Duplicate alias: ' + argConfig.alias) 'Duplicate alias: ' + argConfig.alias)
aliases[argConfig.alias] = arg aliases[argConfig.alias] = argument
} }
if (argConfig.positional) { if (argConfig.positional) {
positional.push(arg) positional.push(argument)
} }
if (argConfig.required) { if (argConfig.required) {
obj[arg] = true obj[argument] = true
} }
return obj return obj
}, {} as Record<string, true>) }, {} as Record<string, true>)
@ -90,12 +184,12 @@ export const argparse = <T extends IArgsConfig>(
return nameOrAlias in config ? nameOrAlias : aliases[nameOrAlias] return nameOrAlias in config ? nameOrAlias : aliases[nameOrAlias]
} }
function processFlags(arg: string): string { function processFlags(argument: string): string {
if (arg.substring(1, 2) === '-') { if (argument.substring(1, 2) === '-') {
return arg.substring(2) return argument.substring(2)
} }
const flags = arg.substring(1).split('') const flags = argument.substring(1).split('')
flags.slice(0, flags.length - 1) flags.slice(0, flags.length - 1)
.forEach(flag => { .forEach(flag => {
@ -120,53 +214,32 @@ export const argparse = <T extends IArgsConfig>(
} }
while (it.hasNext()) { while (it.hasNext()) {
const arg = it.next() const argument = it.next()
const isPositional = arg.substring(0, 1) !== '-' const isPositional = argument.substring(0, 1) !== '-'
const argName = !isPositional const argName = !isPositional
? processFlags(arg) ? processFlags(argument)
: getNextPositional() : getNextPositional()
const argConfig = config[argName] const argConfig = config[argName]
assert(!!argConfig, 'Unknown argument: ' + arg) assert(!!argConfig, 'Unknown argument: ' + argument)
delete requiredArgs[argName] delete requiredArgs[argName]
const peek = it.peek()
switch (argConfig.type) { switch (argConfig.type) {
case 'string': case 'string':
if (isPositional) { result[argName] = getValue(it, argument, isPositional)
result[argName] = arg assert(!!result[argName],
} else { 'Value of argument must be a string: ' + argument)
assert(it.hasNext(), 'Value of argument must be a string: ' + arg) break
result[argName] = it.next()
}
continue
case 'number': case 'number':
const num = parseInt(isPositional ? arg : it.next(), 10) const num = parseInt(getValue(it, argument, isPositional), 10)
assert(!isNaN(num), 'Value of argument must be a number: ' + arg) assert(!isNaN(num), 'Value of argument must be a number: ' + argument)
result[argName] = num result[argName] = num
continue break
case 'boolean': case 'boolean':
if (isPositional) { result[argName] = getBooleanValue(it, argument, isPositional)
if (arg === 'true') { break
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
} else if (peek === 'false') {
it.next()
result[argName] = false
} else {
result[argName] = true
}
continue
default: default:
assert(false, 'Unknown type: ' + argConfig.type) assert(false, 'Unknown type: ' + argConfig.type)
} }
checkChoice(argument, result[argName], argConfig.choices)
} }
assert(!Object.keys(requiredArgs).length, 'Missing required args: ' + assert(!Object.keys(requiredArgs).length, 'Missing required args: ' +