From a1ac8880d495a3b68d25693f159cdfe8ccfb4ab2 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Tue, 6 Aug 2019 11:14:00 +0700 Subject: [PATCH] Add ability to generate help text --- packages/argparse/src/argparse.test.ts | 61 ++++++-- packages/argparse/src/argparse.ts | 200 +++++++++++++------------ 2 files changed, 151 insertions(+), 110 deletions(-) diff --git a/packages/argparse/src/argparse.test.ts b/packages/argparse/src/argparse.test.ts index fa18bad..a4f0076 100644 --- a/packages/argparse/src/argparse.test.ts +++ b/packages/argparse/src/argparse.test.ts @@ -9,7 +9,7 @@ describe('argparse', () => { four: { type: 'boolean', }, - })(['--one', '1', '--two', '2', '--four']) + }).parse(['--one', '1', '--two', '2', '--four']) const one: string = args.one const two: number = args.two @@ -26,7 +26,7 @@ describe('argparse', () => { bool: { type: 'boolean', }, - })([]) + }).parse([]) const value: boolean = result.bool expect(value).toBe(false) }) @@ -36,10 +36,10 @@ describe('argparse', () => { type: 'boolean', required: true, }, - })([])).toThrowError(/Missing required args: bool/) + }).parse([])).toThrowError(/Missing required args: bool/) }) it('optionally accepts a true/false value', () => { - const parse = argparse({ + const {parse} = argparse({ bool: { type: 'boolean', alias: 'b', @@ -57,7 +57,7 @@ describe('argparse', () => { }) }) it('can be grouped by shorthand (single dash) notation', () => { - const parse = argparse({ + const {parse} = argparse({ a1: { type: 'boolean', alias: 'a', @@ -97,7 +97,7 @@ describe('argparse', () => { describe('number', () => { it('sets to NaN by default', () => { - const parse = argparse({ + const {parse} = argparse({ a: { type: 'number', }, @@ -121,7 +121,7 @@ describe('argparse', () => { describe('choices', () => { it('can enforce typed choices', () => { - const parse = argparse({ + const {parse} = argparse({ choice: arg('string', { choices: ['a', 'b'], }), @@ -144,7 +144,7 @@ describe('argparse', () => { describe('positional', () => { it('can be defined', () => { - const parse = argparse({ + const {parse} = argparse({ a: { type: 'number', positional: true, @@ -154,7 +154,7 @@ describe('argparse', () => { expect(parse(['12']).a).toBe(12) }) it('works with booleans', () => { - const parse = argparse({ + const {parse} = argparse({ a: { type: 'boolean', positional: true, @@ -166,7 +166,7 @@ describe('argparse', () => { expect(() => parse(['invalid'])).toThrowError(/true or false/) }) it('works with strings', () => { - const parse = argparse({ + const {parse} = argparse({ a: { type: 'string', positional: true, @@ -176,7 +176,7 @@ describe('argparse', () => { expect(parse(['a']).a).toBe('a') }) it('works with multiple positionals', () => { - const parse = argparse({ + const {parse} = argparse({ a: { type: 'string', positional: true, @@ -192,7 +192,7 @@ describe('argparse', () => { }) }) it('works amongs regular arguments', () => { - const parse = argparse({ + const {parse} = argparse({ arg1: { 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', () => { expect(() => argparse({ one: { type: 'string', required: true, }, - })([])).toThrowError(/missing required/i) + }).parse([])).toThrowError(/missing required/i) }) it('throws when arg type is unknown', () => { @@ -226,7 +259,7 @@ describe('argparse', () => { a: { type: 'test', } as any, - })(['-a'])).toThrowError(/Unknown type: test/) + }).parse(['-a'])).toThrowError(/Unknown type: test/) }) }) diff --git a/packages/argparse/src/argparse.ts b/packages/argparse/src/argparse.ts index 6e4eaba..0ad99cb 100644 --- a/packages/argparse/src/argparse.ts +++ b/packages/argparse/src/argparse.ts @@ -120,7 +120,7 @@ export function padRight(str: string, chars: number) { export function help(config: IArgsConfig) { return Object.keys(config).map(argument => { const argConfig = config[argument] - const {alias, description, type} = argConfig + const {alias, type} = argConfig const name = alias ? `-${alias}, --${argument}` : ` --${argument}` @@ -132,10 +132,12 @@ export function help(config: IArgsConfig) { samples.push('default: ' + argConfig.default) } 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(', ')})` : '' - return padRight(name + ' ' + type, 20) + ' ' + description + sample + return padRight(name + ' ' + type, 30) + ' ' + description + sample }) .join('\n') } @@ -150,100 +152,106 @@ export function arg( } } -export const argparse = ( - config: T, -) => (args: string[]): TArgs => { - const result: any = {} - const it = iterate(args) +export function argparse(config: T) { + return { + help(): string { + return help(config) + }, + parse(args: string[]): TArgs { + const result: any = {} + const it = iterate(args) - const aliases: Record = {} - const positional: string[] = [] - const requiredArgs = Object.keys(config).reduce((obj, argument) => { - const argConfig = config[argument] - result[argument] = argConfig.default !== undefined - ? argConfig.default - : getDefaultValue(argConfig.type) - if (argConfig.alias) { - assert(argConfig.alias.length === 1, - 'Alias must be a single character: ' + argConfig.alias) - assert( - argConfig.alias in aliases === false, - 'Duplicate alias: ' + argConfig.alias) - aliases[argConfig.alias] = argument - } - if (argConfig.positional) { - positional.push(argument) - } - if (argConfig.required) { - obj[argument] = true - } - return obj - }, {} as Record) + const aliases: Record = {} + const positional: string[] = [] + const requiredArgs = Object.keys(config).reduce((obj, argument) => { + const argConfig = config[argument] + result[argument] = argConfig.default !== undefined + ? argConfig.default + : getDefaultValue(argConfig.type) + if (argConfig.alias) { + assert(argConfig.alias.length === 1, + 'Alias must be a single character: ' + argConfig.alias) + assert( + argConfig.alias in aliases === false, + 'Duplicate alias: ' + argConfig.alias) + aliases[argConfig.alias] = argument + } + if (argConfig.positional) { + positional.push(argument) + } + if (argConfig.required) { + obj[argument] = true + } + return obj + }, {} as Record) - function getArgumentName(nameOrAlias: string): string { - return nameOrAlias in config ? nameOrAlias : aliases[nameOrAlias] + function getArgumentName(nameOrAlias: string): string { + 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 }