Add help function
This commit is contained in:
parent
b50dfb1455
commit
8c399c1903
@ -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({
|
||||||
|
|||||||
@ -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: ' +
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user