Add ability to generate help text
This commit is contained in:
parent
8c399c1903
commit
a1ac8880d4
@ -9,7 +9,7 @@ describe('argparse', () => {
|
|||||||
four: {
|
four: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
})(['--one', '1', '--two', '2', '--four'])
|
}).parse(['--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,7 +26,7 @@ describe('argparse', () => {
|
|||||||
bool: {
|
bool: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
})([])
|
}).parse([])
|
||||||
const value: boolean = result.bool
|
const value: boolean = result.bool
|
||||||
expect(value).toBe(false)
|
expect(value).toBe(false)
|
||||||
})
|
})
|
||||||
@ -36,10 +36,10 @@ describe('argparse', () => {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})([])).toThrowError(/Missing required args: bool/)
|
}).parse([])).toThrowError(/Missing required args: bool/)
|
||||||
})
|
})
|
||||||
it('optionally accepts a true/false value', () => {
|
it('optionally accepts a true/false value', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
bool: {
|
bool: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
alias: 'b',
|
alias: 'b',
|
||||||
@ -57,7 +57,7 @@ describe('argparse', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('can be grouped by shorthand (single dash) notation', () => {
|
it('can be grouped by shorthand (single dash) notation', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
a1: {
|
a1: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
alias: 'a',
|
alias: 'a',
|
||||||
@ -97,7 +97,7 @@ describe('argparse', () => {
|
|||||||
|
|
||||||
describe('number', () => {
|
describe('number', () => {
|
||||||
it('sets to NaN by default', () => {
|
it('sets to NaN by default', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
a: {
|
a: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
@ -121,7 +121,7 @@ describe('argparse', () => {
|
|||||||
|
|
||||||
describe('choices', () => {
|
describe('choices', () => {
|
||||||
it('can enforce typed choices', () => {
|
it('can enforce typed choices', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
choice: arg('string', {
|
choice: arg('string', {
|
||||||
choices: ['a', 'b'],
|
choices: ['a', 'b'],
|
||||||
}),
|
}),
|
||||||
@ -144,7 +144,7 @@ describe('argparse', () => {
|
|||||||
|
|
||||||
describe('positional', () => {
|
describe('positional', () => {
|
||||||
it('can be defined', () => {
|
it('can be defined', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
a: {
|
a: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
positional: true,
|
positional: true,
|
||||||
@ -154,7 +154,7 @@ describe('argparse', () => {
|
|||||||
expect(parse(['12']).a).toBe(12)
|
expect(parse(['12']).a).toBe(12)
|
||||||
})
|
})
|
||||||
it('works with booleans', () => {
|
it('works with booleans', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
a: {
|
a: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
positional: true,
|
positional: true,
|
||||||
@ -166,7 +166,7 @@ describe('argparse', () => {
|
|||||||
expect(() => parse(['invalid'])).toThrowError(/true or false/)
|
expect(() => parse(['invalid'])).toThrowError(/true or false/)
|
||||||
})
|
})
|
||||||
it('works with strings', () => {
|
it('works with strings', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
a: {
|
a: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
positional: true,
|
positional: true,
|
||||||
@ -176,7 +176,7 @@ describe('argparse', () => {
|
|||||||
expect(parse(['a']).a).toBe('a')
|
expect(parse(['a']).a).toBe('a')
|
||||||
})
|
})
|
||||||
it('works with multiple positionals', () => {
|
it('works with multiple positionals', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
a: {
|
a: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
positional: true,
|
positional: true,
|
||||||
@ -192,7 +192,7 @@ describe('argparse', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('works amongs regular arguments', () => {
|
it('works amongs regular arguments', () => {
|
||||||
const parse = argparse({
|
const {parse} = argparse({
|
||||||
arg1: {
|
arg1: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
@ -212,13 +212,46 @@ describe('argparse', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('help', () => {
|
||||||
|
it('returns help string', () => {
|
||||||
|
const {help} = argparse({
|
||||||
|
one: arg('string'),
|
||||||
|
two: arg('number'),
|
||||||
|
three: arg('boolean'),
|
||||||
|
})
|
||||||
|
expect(help()).toEqual([
|
||||||
|
' --one string ',
|
||||||
|
' --two number ',
|
||||||
|
' --three boolean ',
|
||||||
|
].join('\n'))
|
||||||
|
})
|
||||||
|
it('returns help string with alias, description, and samples', () => {
|
||||||
|
const {help} = argparse({
|
||||||
|
one: arg('string', {
|
||||||
|
description: 'first argument',
|
||||||
|
required: true,
|
||||||
|
choices: ['choice-1', 'choice-2'],
|
||||||
|
default: 'choice-1',
|
||||||
|
alias: 'o',
|
||||||
|
}),
|
||||||
|
two: arg('number'),
|
||||||
|
})
|
||||||
|
expect(help()).toEqual([
|
||||||
|
'-o, --one string first argument ' +
|
||||||
|
'(required, default: choice-1, choices: choice-1,choice-2)',
|
||||||
|
' --two number ',
|
||||||
|
].join('\n'))
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
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)
|
}).parse([])).toThrowError(/missing required/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws when arg type is unknown', () => {
|
it('throws when arg type is unknown', () => {
|
||||||
@ -226,7 +259,7 @@ describe('argparse', () => {
|
|||||||
a: {
|
a: {
|
||||||
type: 'test',
|
type: 'test',
|
||||||
} as any,
|
} as any,
|
||||||
})(['-a'])).toThrowError(/Unknown type: test/)
|
}).parse(['-a'])).toThrowError(/Unknown type: test/)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -120,7 +120,7 @@ export function padRight(str: string, chars: number) {
|
|||||||
export function help(config: IArgsConfig) {
|
export function help(config: IArgsConfig) {
|
||||||
return Object.keys(config).map(argument => {
|
return Object.keys(config).map(argument => {
|
||||||
const argConfig = config[argument]
|
const argConfig = config[argument]
|
||||||
const {alias, description, type} = argConfig
|
const {alias, type} = argConfig
|
||||||
const name = alias
|
const name = alias
|
||||||
? `-${alias}, --${argument}`
|
? `-${alias}, --${argument}`
|
||||||
: ` --${argument}`
|
: ` --${argument}`
|
||||||
@ -132,10 +132,12 @@ export function help(config: IArgsConfig) {
|
|||||||
samples.push('default: ' + argConfig.default)
|
samples.push('default: ' + argConfig.default)
|
||||||
}
|
}
|
||||||
if (argConfig.choices) {
|
if (argConfig.choices) {
|
||||||
samples.push('choices: ' + argConfig.choices)
|
samples.push('choices: ' + argConfig.choices.join(','))
|
||||||
}
|
}
|
||||||
|
const description = argConfig.description
|
||||||
|
? ' ' + argConfig.description : ''
|
||||||
const sample = samples.length ? ` (${samples.join(', ')})` : ''
|
const sample = samples.length ? ` (${samples.join(', ')})` : ''
|
||||||
return padRight(name + ' ' + type, 20) + ' ' + description + sample
|
return padRight(name + ' ' + type, 30) + ' ' + description + sample
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
@ -150,100 +152,106 @@ export function arg<T extends TArgTypeName>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const argparse = <T extends IArgsConfig>(
|
export function argparse<T extends IArgsConfig>(config: T) {
|
||||||
config: T,
|
return {
|
||||||
) => (args: string[]): TArgs<T> => {
|
help(): string {
|
||||||
const result: any = {}
|
return help(config)
|
||||||
const it = iterate(args)
|
},
|
||||||
|
parse(args: string[]): TArgs<T> {
|
||||||
|
const result: any = {}
|
||||||
|
const it = iterate(args)
|
||||||
|
|
||||||
const aliases: Record<string, string> = {}
|
const aliases: Record<string, string> = {}
|
||||||
const positional: string[] = []
|
const positional: string[] = []
|
||||||
const requiredArgs = Object.keys(config).reduce((obj, argument) => {
|
const requiredArgs = Object.keys(config).reduce((obj, argument) => {
|
||||||
const argConfig = config[argument]
|
const argConfig = config[argument]
|
||||||
result[argument] = 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,
|
assert(argConfig.alias.length === 1,
|
||||||
'Alias must be a single character: ' + argConfig.alias)
|
'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] = argument
|
aliases[argConfig.alias] = argument
|
||||||
}
|
}
|
||||||
if (argConfig.positional) {
|
if (argConfig.positional) {
|
||||||
positional.push(argument)
|
positional.push(argument)
|
||||||
}
|
}
|
||||||
if (argConfig.required) {
|
if (argConfig.required) {
|
||||||
obj[argument] = true
|
obj[argument] = true
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
}, {} as Record<string, true>)
|
}, {} as Record<string, true>)
|
||||||
|
|
||||||
function getArgumentName(nameOrAlias: string): string {
|
function getArgumentName(nameOrAlias: string): string {
|
||||||
return nameOrAlias in config ? nameOrAlias : aliases[nameOrAlias]
|
return nameOrAlias in config ? nameOrAlias : aliases[nameOrAlias]
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFlags(argument: string): string {
|
||||||
|
if (argument.substring(1, 2) === '-') {
|
||||||
|
return argument.substring(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const flags = argument.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
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextPositional(): string {
|
||||||
|
const p = positional.shift()
|
||||||
|
assert(!!p, 'No defined positional arguments')
|
||||||
|
return p!
|
||||||
|
}
|
||||||
|
|
||||||
|
while (it.hasNext()) {
|
||||||
|
const argument = it.next()
|
||||||
|
const isPositional = argument.substring(0, 1) !== '-'
|
||||||
|
const argName = !isPositional
|
||||||
|
? processFlags(argument)
|
||||||
|
: getNextPositional()
|
||||||
|
const argConfig = config[argName]
|
||||||
|
assert(!!argConfig, 'Unknown argument: ' + argument)
|
||||||
|
delete requiredArgs[argName]
|
||||||
|
switch (argConfig.type) {
|
||||||
|
case 'string':
|
||||||
|
result[argName] = getValue(it, argument, isPositional)
|
||||||
|
assert(!!result[argName],
|
||||||
|
'Value of argument must be a string: ' + argument)
|
||||||
|
break
|
||||||
|
case 'number':
|
||||||
|
const num = parseInt(getValue(it, argument, isPositional), 10)
|
||||||
|
assert(!isNaN(num),
|
||||||
|
'Value of argument must be a number: ' + argument)
|
||||||
|
result[argName] = num
|
||||||
|
break
|
||||||
|
case 'boolean':
|
||||||
|
result[argName] = getBooleanValue(it, argument, isPositional)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
assert(false, 'Unknown type: ' + argConfig.type)
|
||||||
|
}
|
||||||
|
checkChoice(argument, result[argName], argConfig.choices)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!Object.keys(requiredArgs).length, 'Missing required args: ' +
|
||||||
|
Object.keys(requiredArgs).join(', '))
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function processFlags(argument: string): string {
|
|
||||||
if (argument.substring(1, 2) === '-') {
|
|
||||||
return argument.substring(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const flags = argument.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
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNextPositional(): string {
|
|
||||||
const p = positional.shift()
|
|
||||||
assert(!!p, 'No defined positional arguments')
|
|
||||||
return p!
|
|
||||||
}
|
|
||||||
|
|
||||||
while (it.hasNext()) {
|
|
||||||
const argument = it.next()
|
|
||||||
const isPositional = argument.substring(0, 1) !== '-'
|
|
||||||
const argName = !isPositional
|
|
||||||
? processFlags(argument)
|
|
||||||
: getNextPositional()
|
|
||||||
const argConfig = config[argName]
|
|
||||||
assert(!!argConfig, 'Unknown argument: ' + argument)
|
|
||||||
delete requiredArgs[argName]
|
|
||||||
switch (argConfig.type) {
|
|
||||||
case 'string':
|
|
||||||
result[argName] = getValue(it, argument, isPositional)
|
|
||||||
assert(!!result[argName],
|
|
||||||
'Value of argument must be a string: ' + argument)
|
|
||||||
break
|
|
||||||
case 'number':
|
|
||||||
const num = parseInt(getValue(it, argument, isPositional), 10)
|
|
||||||
assert(!isNaN(num), 'Value of argument must be a number: ' + argument)
|
|
||||||
result[argName] = num
|
|
||||||
break
|
|
||||||
case 'boolean':
|
|
||||||
result[argName] = getBooleanValue(it, argument, isPositional)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
assert(false, 'Unknown type: ' + argConfig.type)
|
|
||||||
}
|
|
||||||
checkChoice(argument, result[argName], argConfig.choices)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(!Object.keys(requiredArgs).length, 'Missing required args: ' +
|
|
||||||
Object.keys(requiredArgs).join(', '))
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user