diff --git a/packages/argparse/src/argparse.test.ts b/packages/argparse/src/argparse.test.ts index 07fd90f..d0d097f 100644 --- a/packages/argparse/src/argparse.test.ts +++ b/packages/argparse/src/argparse.test.ts @@ -2,6 +2,15 @@ import {argparse, arg, IArgsConfig} from './argparse' describe('argparse', () => { + const CMD = 'command' + const exit = jest.fn() + const log = jest.fn() + + beforeEach(() => { + exit.mockClear() + log.mockClear() + }) + it('parses args', () => { const args = argparse({ one: arg('string', {required: true}), @@ -9,7 +18,7 @@ describe('argparse', () => { four: { type: 'boolean', }, - }).parse(['--one', '1', '--two', '2', '--four']) + }).parse([CMD, '--one', '1', '--two', '2', '--four']) const one: string = args.one const two: number = args.two @@ -26,7 +35,7 @@ describe('argparse', () => { bool: { type: 'boolean', }, - }).parse([]) + }).parse([CMD]) const value: boolean = result.bool expect(value).toBe(false) }) @@ -36,7 +45,7 @@ describe('argparse', () => { type: 'boolean', required: true, }, - }).parse([])).toThrowError(/Missing required args: bool/) + }).parse([CMD])).toThrowError(/Missing required args: bool/) }) it('optionally accepts a true/false value', () => { const {parse} = argparse({ @@ -48,10 +57,10 @@ describe('argparse', () => { type: 'string', }, }) - expect(parse(['--bool']).bool).toBe(true) - expect(parse(['--bool', 'false']).bool).toBe(false) - expect(parse(['--bool', 'true']).bool).toBe(true) - expect(parse(['--bool', '--other', 'value'])).toEqual({ + expect(parse([CMD, '--bool']).bool).toBe(true) + expect(parse([CMD, '--bool', 'false']).bool).toBe(false) + expect(parse([CMD, '--bool', 'true']).bool).toBe(true) + expect(parse([CMD, '--bool', '--other', 'value'])).toEqual({ bool: true, other: 'value', }) @@ -71,27 +80,27 @@ describe('argparse', () => { alias: 'o', }, }) - expect(parse([])).toEqual({ + expect(parse([CMD])).toEqual({ a1: false, b: false, other: '', }) - expect(parse(['-ab'])).toEqual({ + expect(parse([CMD, '-ab'])).toEqual({ a1: true, b: true, other: '', }) - expect(parse(['-ca'])).toEqual({ + expect(parse([CMD, '-ca'])).toEqual({ a1: true, b: true, other: '', }) - expect(parse(['-abo', 'test'])).toEqual({ + expect(parse([CMD, '-abo', 'test'])).toEqual({ a1: true, b: true, other: 'test', }) - expect(() => parse(['-abo'])).toThrowError(/must be a string: -abo/) + expect(() => parse([CMD, '-abo'])).toThrowError(/must be a string: -abo/) }) }) @@ -102,18 +111,18 @@ describe('argparse', () => { type: 'number', }, }) - expect(parse([])).toEqual({ + expect(parse([CMD])).toEqual({ a: NaN, }) - expect(() => parse(['-a'])).toThrowError(/must be a number: -a/) - expect(() => parse(['-a', 'no-number'])) + expect(() => parse([CMD, '-a'])).toThrowError(/must be a number: -a/) + expect(() => parse([CMD, '-a', 'no-number'])) .toThrowError(/must be a number: -a/) - expect(() => parse(['--a', 'no-number'])) + expect(() => parse([CMD, '--a', 'no-number'])) .toThrowError(/must be a number: --a/) - expect(parse(['-a', '10'])).toEqual({ + expect(parse([CMD, '-a', '10'])).toEqual({ a: 10, }) - expect(parse(['--a', '11'])).toEqual({ + expect(parse([CMD, '--a', '11'])).toEqual({ a: 11, }) }) @@ -129,13 +138,14 @@ describe('argparse', () => { 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({ + expect(() => parse([CMD, '--choice', 'c'])).toThrowError(/one of: a, b$/) + expect(() => parse([CMD, '--num', '3'])) + .toThrowError(/must be one of: 1, 2$/) + expect(parse([CMD, '--choice', 'a', '--num', '1'])).toEqual({ choice: 'a', num: 1, }) - expect(parse(['--choice', 'b', '--num', '2'])).toEqual({ + expect(parse([CMD, '--choice', 'b', '--num', '2'])).toEqual({ choice: 'b', num: 2, }) @@ -144,22 +154,25 @@ describe('argparse', () => { describe('string[] and n', () => { it('has a value of n = 1 by default', () => { - const {parse, help} = argparse({ + const {parse} = argparse({ value: { type: 'string[]', }, - }) - expect(parse([]).value).toEqual([]) - expect(parse(['--value', 'one']).value).toEqual(['one']) - expect(help()).toEqual([ - '[OPTIONS] ', + help: arg('boolean'), + }, exit, log) + expect(parse([CMD]).value).toEqual([]) + expect(parse([CMD, '--value', 'one']).value).toEqual(['one']) + parse([CMD, '--help']) + expect(log.mock.calls[0][0]).toEqual([ + `${CMD} [OPTIONS] `, '', 'Options:', ' --value [VALUE] ', + ' --help boolean ', ].join('\n')) }) it('can be used to extract finite number of values', () => { - const {parse, help} = argparse({ + const {parse} = argparse({ value: { type: 'string[]', n: 3, @@ -168,68 +181,84 @@ describe('argparse', () => { type: 'number', alias: 'o', }, - }) - expect(parse([]).value).toEqual([]) - expect(parse(['--value', 'a', 'b', '--other', '-o', '3'])).toEqual({ + help: arg('boolean'), + }, exit, log) + expect(parse([CMD]).value).toEqual([]) + expect(parse([CMD, '--value', 'a', 'b', '--other', '-o', '3'])).toEqual({ value: ['a', 'b', '--other'], other: 3, + help: false, }) - expect(help()).toEqual([ - '[OPTIONS] ', + parse([CMD, '--help']) + expect(log.mock.calls[0][0]).toEqual([ + `${CMD} [OPTIONS] `, '', 'Options:', ' --value [VALUE1 VALUE2 VALUE3] ', '-o, --other number ', + ' --help boolean ', ].join('\n')) }) it('can be used to collect any remaining arguments when n = "+"', () => { - const {parse, help} = argparse({ + const {parse} = argparse({ value: arg('string[]', {n: '+', required: true}), other: arg('number'), - }) - expect(() => parse([])).toThrowError(/Missing required args: value/) - expect(parse(['--value', 'a', '--other', '3'])).toEqual({ + help: arg('boolean'), + }, exit, log) + expect(() => parse([CMD])).toThrowError(/Missing required args: value/) + expect(parse([CMD, '--value', 'a', '--other', '3'])).toEqual({ value: ['a', '--other', '3'], other: NaN, + help: false, }) - expect(parse(['--other', '2', '--value', 'a', '--other', '3'])).toEqual({ + expect(parse([CMD, '--other', '2', '--value', 'a', '--other', '3'])) + .toEqual({ value: ['a', '--other', '3'], other: 2, + help: false, }) - expect(help()).toEqual([ - '[OPTIONS] ', + parse([CMD, '--help']) + expect(log.mock.calls[0][0]).toEqual([ + `${CMD} [OPTIONS] `, '', 'Options:', ' --value VALUE... (required)', ' --other number ', + ' --help boolean ', ].join('\n')) }) it('can collect remaining positional arguments when n = "*"', () => { - const {parse, help} = argparse({ + const {parse} = argparse({ value: arg('string[]', {n: '*', required: true, positional: true}), other: arg('number'), - }) - expect(parse(['a', 'b']).value).toEqual(['a', 'b']) - expect(() => parse(['--other', '3']).value) + help: arg('boolean'), + }, exit, log) + expect(parse([CMD, 'a', 'b']).value).toEqual(['a', 'b']) + expect(() => parse([CMD, '--other', '3']).value) .toThrowError(/Missing.*: value/) - expect(parse(['--other', '2', '--', '--other', '3'])).toEqual({ + expect(parse([CMD, '--other', '2', '--', '--other', '3'])).toEqual({ value: ['--other', '3'], other: 2, + help: false, }) - expect(parse(['--', '--other', '3'])).toEqual({ + expect(parse([CMD, '--', '--other', '3'])).toEqual({ value: ['--other', '3'], other: NaN, + help: false, }) - expect(parse(['--other', '3', 'a', 'b', 'c'])).toEqual({ + expect(parse([CMD, '--other', '3', 'a', 'b', 'c'])).toEqual({ value: ['a', 'b', 'c'], other: 3, + help: false, }) - expect(help()).toEqual([ - '[OPTIONS] [VALUE...]', + parse([CMD, '--help']) + expect(log.mock.calls[0][0]).toEqual([ + `${CMD} [OPTIONS] [VALUE...]`, '', 'Options:', ' --value [VALUE...] (required)', ' --other number ', + ' --help boolean ', ].join('\n')) }) }) @@ -242,8 +271,8 @@ describe('argparse', () => { positional: true, }, }) - expect(parse([]).a).toBe(NaN) - expect(parse(['12']).a).toBe(12) + expect(parse([CMD]).a).toBe(NaN) + expect(parse([CMD, '12']).a).toBe(12) }) it('works with booleans', () => { const {parse} = argparse({ @@ -252,10 +281,10 @@ describe('argparse', () => { positional: true, }, }) - expect(parse([]).a).toBe(false) - expect(parse(['true']).a).toBe(true) - expect(parse(['false']).a).toBe(false) - expect(() => parse(['invalid'])).toThrowError(/true or false/) + expect(parse([CMD]).a).toBe(false) + expect(parse([CMD, 'true']).a).toBe(true) + expect(parse([CMD, 'false']).a).toBe(false) + expect(() => parse([CMD, 'invalid'])).toThrowError(/true or false/) }) it('works with strings', () => { const {parse} = argparse({ @@ -264,8 +293,8 @@ describe('argparse', () => { positional: true, }, }) - expect(parse([]).a).toBe('') - expect(parse(['a']).a).toBe('a') + expect(parse([CMD]).a).toBe('') + expect(parse([CMD, 'a']).a).toBe('a') }) it('works with multiple positionals', () => { const {parse} = argparse({ @@ -278,7 +307,7 @@ describe('argparse', () => { positional: true, }, }) - expect(parse(['aaa', 'bbb'])).toEqual({ + expect(parse([CMD, 'aaa', 'bbb'])).toEqual({ a: 'aaa', b: 'bbb', }) @@ -296,7 +325,7 @@ describe('argparse', () => { type: 'string', }, }) - expect(parse(['--arg1', 'one', '2', '--arg3', 'three'])).toEqual({ + expect(parse([CMD, '--arg1', 'one', '2', '--arg3', 'three'])).toEqual({ arg1: 'one', arg2: 2, arg3: 'three', @@ -305,23 +334,28 @@ describe('argparse', () => { }) describe('help', () => { - it('returns help string', () => { - const {help} = argparse({ + it('prints help string and exits', () => { + const {parse} = argparse({ one: arg('string'), two: arg('number'), three: arg('boolean'), - }) - expect(help()).toEqual([ - '[OPTIONS] ', + help: arg('boolean'), + }, exit, log) + expect(exit.mock.calls.length).toBe(0) + parse([CMD, '--help']) + expect(exit.mock.calls.length).toBe(1) + expect(log.mock.calls[0][0]).toEqual([ + `${CMD} [OPTIONS] `, '', 'Options:', ' --one string ', ' --two number ', ' --three boolean ', + ' --help boolean ', ].join('\n')) }) it('returns help string with alias, description, and samples', () => { - const {help} = argparse({ + const {parse} = argparse({ one: arg('string', { description: 'first argument', required: true, @@ -336,15 +370,20 @@ describe('argparse', () => { three: arg('number', { positional: true, }), - }) - expect(help()).toEqual([ - '[OPTIONS] TWO [THREE]', + help: arg('boolean'), + }, exit, log) + expect(exit.mock.calls.length).toBe(0) + parse([CMD, '--help']) + expect(exit.mock.calls.length).toBe(1) + expect(log.mock.calls[0][0]).toEqual([ + `${CMD} [OPTIONS] TWO [THREE]`, '', 'Options:', '-o, --one string first argument ' + '(required, default: choice-1, choices: choice-1,choice-2)', ' --two number (required)', ' --three number ', + ' --help boolean ', ].join('\n')) }) @@ -356,7 +395,7 @@ describe('argparse', () => { type: 'string', required: true, }, - }).parse([])).toThrowError(/missing required/i) + }).parse([CMD])).toThrowError(/missing required/i) }) it('throws when arg type is unknown', () => { @@ -364,7 +403,7 @@ describe('argparse', () => { a: { type: 'test', } as any, - }).parse(['-a'])).toThrowError(/Unknown type: test/) + }).parse([CMD, '-a'])).toThrowError(/Unknown type: test/) }) }) diff --git a/packages/argparse/src/argparse.ts b/packages/argparse/src/argparse.ts index b22be79..02d473e 100644 --- a/packages/argparse/src/argparse.ts +++ b/packages/argparse/src/argparse.ts @@ -16,8 +16,6 @@ export const N_DEFAULT_VALUE = 1 export type TNumberOfArgs = number | '+' | '*' -export let exit = () => process.exit() - export interface IArgParam { alias?: string description?: string @@ -142,10 +140,6 @@ function extractArray( return array } -export function isHelp(argv: string[]) { - return argv.some(a => /^(-h|--help)$/.test(a)) -} - function checkChoice(argument: string, choice: T, choices?: T[]) { if (choices) { assert( @@ -161,7 +155,7 @@ export function padRight(str: string, chars: number) { return str } -export function help(config: IArgsConfig) { +export function help(command: string, config: IArgsConfig) { const keys = Object.keys(config) function getArrayHelp( @@ -189,6 +183,7 @@ export function help(config: IArgsConfig) { } const positionalHelp = [ + command, '[OPTIONS]', keys .filter(k => config[k].positional) @@ -237,12 +232,16 @@ export function arg( } } -export function argparse(config: T) { +export function argparse( + config: T, + exit: () => void = () => process.exit(), + /* tslint:disable-next-line */ + log: (message: string) => void = console.log.bind(console), +) { return { - help(): string { - return help(config) - }, parse(args: string[]): TArgs { + const command = args[0] + args = args.slice(1) const result: any = {} const it = iterate(args) @@ -315,6 +314,12 @@ export function argparse(config: T) { ? processFlags(argument) : getNextPositional() const argConfig = config[argName] + if (!isPositional && argName === 'help') { + log(help(command, config)) + exit() + // should never reach this in real life + return null as any + } assert(!!argConfig, 'Unknown argument: ' + argument) delete requiredArgs[argName] switch (argConfig.type) { diff --git a/packages/scripts/src/argparse.ts b/packages/scripts/src/argparse.ts deleted file mode 100644 index d268798..0000000 --- a/packages/scripts/src/argparse.ts +++ /dev/null @@ -1,22 +0,0 @@ -export {arg} from '@rondo/argparse' -import {info} from './log' -import { - argparse as configure, IArgsConfig, isHelp, TArgs -} from '@rondo/argparse' - -export let exit = () => process.exit() - -export function argparse(config: T) { - const parser = configure(config) - return { - ...parser, - parse(args: string[]): TArgs { - const result = parser.parse(args) - if ('help' in config && isHelp(args)) { - info(parser.help()) - exit() - } - return result - }, - } -} diff --git a/packages/scripts/src/commands/build.ts b/packages/scripts/src/commands/build.ts index 557a199..aeea2dc 100644 --- a/packages/scripts/src/commands/build.ts +++ b/packages/scripts/src/commands/build.ts @@ -1,7 +1,7 @@ import * as fs from 'fs' import * as log from '../log' import * as p from 'path' -import {argparse, arg} from '../argparse' +import {argparse, arg} from '@rondo/argparse' import {findNodeModules} from '../modules' import {join} from 'path' import {run} from '../run' @@ -9,7 +9,7 @@ import {run} from '../run' const tsc = 'ttsc' export async function build(...argv: string[]) { - const {parse, help} = argparse({ + const {parse} = argparse({ project: arg('string', { alias: 'p', default: '.', diff --git a/packages/scripts/src/commands/newlib.ts b/packages/scripts/src/commands/newlib.ts index 5306289..d3f87d3 100644 --- a/packages/scripts/src/commands/newlib.ts +++ b/packages/scripts/src/commands/newlib.ts @@ -1,11 +1,11 @@ import * as fs from 'fs' import * as log from '../log' import * as path from 'path' -import {argparse, arg} from '../argparse' +import {argparse, arg} from '@rondo/argparse' import {run} from '../run' export async function newlib(...argv: string[]) { - const {parse, help} = argparse({ + const {parse} = argparse({ name: arg('string', {positional: true, required: true}), namespace: arg('string', {default: '@rondo'}), help: arg('boolean', { diff --git a/packages/scripts/src/commands/typecheck.ts b/packages/scripts/src/commands/typecheck.ts index 222eb59..4e5ce84 100644 --- a/packages/scripts/src/commands/typecheck.ts +++ b/packages/scripts/src/commands/typecheck.ts @@ -1,6 +1,6 @@ import * as fs from 'fs' import * as ts from 'typescript' -import {argparse, arg} from '../argparse' +import {argparse, arg} from '@rondo/argparse' function isObjectType(type: ts.Type): type is ts.ObjectType { return !!(type.flags & ts.TypeFlags.Object) @@ -210,11 +210,9 @@ export function typecheck(...argv: string[]) { if (typeDefinitions.has(type)) { return } - // if (type.aliasSymbol) { - // // TODO figure out how to prevent iterating of properties from types - // // such as strings - // return - // } + if (type.aliasSymbol) { + throw new Error('Type aliases are not supported') + } const typeParameters: ts.TypeParameter[] = [] const expandedTypeParameters: ts.Type[] = [] const allRelevantTypes: ts.Type[] = [] diff --git a/packages/scripts/src/index.ts b/packages/scripts/src/index.ts index d1e2020..9e1f199 100644 --- a/packages/scripts/src/index.ts +++ b/packages/scripts/src/index.ts @@ -2,22 +2,32 @@ import * as commands from './commands' import * as log from './log' import {TCommand} from './TCommand' +import {argparse, arg} from '@rondo/argparse' -async function run(...argv: string[]) { - const commandName = argv[0] - if (!(commandName in commands)) { +const {parse} = argparse({ + help: arg('boolean'), + debug: arg('boolean'), + command: arg('string', {required: true, positional: true}), + other: arg('string[]', {n: '*', positional: true}), +}) + +type TArgs = ReturnType + +async function run(args: TArgs) { + if (!(args.command in commands)) { const c = Object.keys(commands).filter(cmd => !cmd.startsWith('_')) log.info(`Available commands:\n\n${c.join('\n')}`) return } - const command = (commands as any)[commandName] as TCommand - await command(...argv.slice(1)) + const command = (commands as any)[args.command] as TCommand + await command(args.command, ...args.other) } if (typeof require !== 'undefined' && require.main === module) { - run(...process.argv.slice(2)) + const args = parse(process.argv.slice(1)) + run(args) .catch(err => { - log.error('> ' + err.message) + log.error('> ' + (args.debug ? err.stack : err.message)) process.exit(1) }) }