From 64d244fe9189ccbc22db3c342f491da58eed2d92 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Wed, 14 Aug 2019 10:17:06 +0700 Subject: [PATCH] Add better help for positional args --- packages/argparse/src/argparse.test.ts | 50 +++++++++------ packages/argparse/src/argparse.ts | 84 ++++++++++++++++++-------- packages/scripts/src/index.ts | 35 ++++++++--- 3 files changed, 116 insertions(+), 53 deletions(-) diff --git a/packages/argparse/src/argparse.test.ts b/packages/argparse/src/argparse.test.ts index 4142369..92fb744 100644 --- a/packages/argparse/src/argparse.test.ts +++ b/packages/argparse/src/argparse.test.ts @@ -164,11 +164,11 @@ describe('argparse', () => { expect(parse([CMD, '--value', 'one']).value).toEqual(['one']) parse([CMD, '--help']) expect(log.mock.calls[0][0]).toEqual([ - `${CMD} [OPTIONS] `, + `${CMD} [OPTIONS]`, '', 'Options:', - ' --value [VALUE] ', - ' --help boolean ', + ' --value [VALUE]', + ' --help boolean', ].join('\n')) }) it('can be used to extract finite number of values', () => { @@ -191,12 +191,12 @@ describe('argparse', () => { }) parse([CMD, '--help']) expect(log.mock.calls[0][0]).toEqual([ - `${CMD} [OPTIONS] `, + `${CMD} [OPTIONS]`, '', 'Options:', - ' --value [VALUE1 VALUE2 VALUE3] ', - '-o, --other number ', - ' --help boolean ', + ' --value [VALUE1 VALUE2 VALUE3]', + '-o, --other number', + ' --help boolean', ].join('\n')) }) it('can be used to collect any remaining arguments when n = "+"', () => { @@ -219,12 +219,12 @@ describe('argparse', () => { }) parse([CMD, '--help']) expect(log.mock.calls[0][0]).toEqual([ - `${CMD} [OPTIONS] `, + `${CMD} [OPTIONS]`, '', 'Options:', ' --value VALUE... (required)', - ' --other number ', - ' --help boolean ', + ' --other number', + ' --help boolean', ].join('\n')) }) it('can collect remaining positional arguments when n = "*"', () => { @@ -255,9 +255,12 @@ describe('argparse', () => { expect(log.mock.calls[0][0]).toEqual([ `${CMD} [OPTIONS] [VALUE...]`, '', + 'Positional arguments:', + ' VALUE string[] (required)', + '', 'Options:', - ' --other number ', - ' --help boolean ', + ' --other number', + ' --help boolean', ].join('\n')) }) }) @@ -269,9 +272,14 @@ describe('argparse', () => { type: 'number', positional: true, }, - }) + }, exit, log) expect(parse([CMD]).a).toBe(NaN) expect(parse([CMD, '12']).a).toBe(12) + parse([CMD, '--help']) + expect(log.mock.calls[0][0]).toEqual(`${CMD} [A] + +Positional arguments: + A number`) }) it('works with booleans', () => { const {parse} = argparse({ @@ -344,13 +352,13 @@ describe('argparse', () => { parse([CMD, '--help']) expect(exit.mock.calls.length).toBe(1) expect(log.mock.calls[0][0]).toEqual([ - `${CMD} [OPTIONS] `, + `${CMD} [OPTIONS]`, '', 'Options:', - ' --one string ', - ' --two number ', - ' --three boolean ', - ' --help boolean ', + ' --one string', + ' --two number', + ' --three boolean', + ' --help boolean', ].join('\n')) }) it('returns help string with alias, description, and samples', () => { @@ -377,10 +385,14 @@ describe('argparse', () => { expect(log.mock.calls[0][0]).toEqual([ `${CMD} [OPTIONS] TWO [THREE]`, '', + 'Positional arguments:', + ' TWO number (required)', + ' THREE number', + '', 'Options:', '-o, --one string first argument ' + '(required, default: choice-1, choices: choice-1,choice-2)', - ' --help boolean ', + ' --help boolean', ].join('\n')) }) diff --git a/packages/argparse/src/argparse.ts b/packages/argparse/src/argparse.ts index 7295af3..914de2f 100644 --- a/packages/argparse/src/argparse.ts +++ b/packages/argparse/src/argparse.ts @@ -184,23 +184,7 @@ export function help(command: string, config: IArgsConfig) { return required ? array.join(' ') : `[${array.join(' ')}]` } - const positionalHelp = [ - relative(process.cwd(), command), - '[OPTIONS]', - keys - .filter(k => config[k].positional) - .map(k => getArrayHelp(k, config[k].required, config[k].n)) - .join(' '), - ].join(' ') - - const options = keys.filter(k => !config[k].positional) - - const argsHelp = 'Options:\n' + options.map(argument => { - const argConfig = config[argument] - const {alias, type} = argConfig - const name = alias - ? `-${alias}, --${argument}` - : ` --${argument}` + function getDescription(argConfig: IArgument): string { const samples = [] if (argConfig.required) { samples.push('required') @@ -214,14 +198,66 @@ export function help(command: string, config: IArgsConfig) { const description = argConfig.description ? ' ' + argConfig.description : '' const sample = samples.length ? ` (${samples.join(', ')})` : '' - const argType = type === 'string[]' - ? getArrayHelp(argument, argConfig.required, argConfig.n) - : type - return padRight(name + ' ' + argType, 30) + ' ' + description + sample - }) - .join('\n') + return description + sample + } - return [positionalHelp, argsHelp] + function getPaddedName(nameAndType: string, description: string) { + return description + ? padRight(nameAndType, 30) + ' ' + description + : nameAndType + } + + function getArgType( + type: TArgTypeName, argument: string, required?: boolean, n?: TNumberOfArgs, + ): string { + return type === 'string[]' + ? getArrayHelp(argument, required, n) + : type + } + + const positionalArgs = keys + .filter(k => config[k].positional) + .map(argument => { + const argConfig = config[argument] + const {type, required, n} = argConfig + const nameAndType = ` ${argument.toUpperCase()} ${type}` + const description = getDescription(argConfig) + return getPaddedName(nameAndType, description) + }) + + const options = keys + .filter(k => !config[k].positional) + .map(argument => { + const argConfig = config[argument] + const {alias, type, required, n} = argConfig + const name = alias + ? `-${alias}, --${argument}` + : ` --${argument}` + const description = getDescription(argConfig) + const argType = getArgType(type, argument, required, n) + const nameAndType = `${name} ${argType}` + return getPaddedName(nameAndType, description) + }) + + const positionalHelp = positionalArgs.length + ? 'Positional arguments:\n' + positionalArgs.join('\n') + : '' + const optionsHelp = options.length + ? 'Options:\n' + options.join('\n') + : '' + + const commandHelp = [ + relative(process.cwd(), command), + options.length ? '[OPTIONS]' : '', + keys + .filter(k => config[k].positional) + .map(k => getArrayHelp(k, config[k].required, config[k].n)) + .join(' '), + ] + .filter(k => k.length) + .join(' ') + + return [commandHelp, positionalHelp, optionsHelp] .filter(h => h.length) .join('\n\n') } diff --git a/packages/scripts/src/index.ts b/packages/scripts/src/index.ts index d21fdb9..79e8856 100644 --- a/packages/scripts/src/index.ts +++ b/packages/scripts/src/index.ts @@ -5,29 +5,44 @@ import {TCommand} from './TCommand' import {argparse, arg} from '@rondo/argparse' const {parse} = argparse({ - help: arg('boolean'), + help: arg('boolean', {alias: 'h'}), debug: arg('boolean'), - command: arg('string[]', {n: '+', required: true, positional: true}), + command: arg('string[]', { + n: '+', + required: true, + positional: true, + description: 'Must be one of: ' + Object.keys(commands).join(', '), + }), }) type TArgs = ReturnType -async function run(args: TArgs) { +async function run(args: TArgs, exit: (code: number) => void) { const commandName = args.command[0] if (!(commandName in commands)) { const c = Object.keys(commands).filter(cmd => !cmd.startsWith('_')) log.info(`Available commands:\n\n${c.join('\n')}`) + exit(1) return } const command = (commands as any)[commandName] as TCommand await command(...args.command) } -if (typeof require !== 'undefined' && require.main === module) { - const args = parse(process.argv.slice(1)) - run(args) - .catch(err => { - log.error('> ' + (args.debug ? err.stack : err.message)) - process.exit(1) - }) +async function start( + argv: string[] = process.argv.slice(1), + exit = (code: number) => process.exit(code), +) { + let args: TArgs | null = null + try { + args = parse(argv) + await run(args, exit) + } catch (err) { + log.error((args && args.debug ? err.stack : err.message)) + exit(1) + } +} + +if (typeof require !== 'undefined' && require.main === module) { + start() }