From b50dfb14553923608dda35cad9dc5db48e228bc8 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Mon, 5 Aug 2019 22:56:40 +0700 Subject: [PATCH] Add support for positional args in @rondo/argparse --- packages/argparse/src/argparse.test.ts | 70 ++++++++++++++++++++++++++ packages/argparse/src/argparse.ts | 37 ++++++++++++-- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/packages/argparse/src/argparse.test.ts b/packages/argparse/src/argparse.test.ts index 06df487..f374630 100644 --- a/packages/argparse/src/argparse.test.ts +++ b/packages/argparse/src/argparse.test.ts @@ -125,6 +125,76 @@ describe('argparse', () => { }) }) + describe('positional', () => { + it('can be defined', () => { + const parse = argparse({ + a: { + type: 'number', + positional: true, + }, + }) + expect(parse([]).a).toBe(NaN) + expect(parse(['12']).a).toBe(12) + }) + it('works with booleans', () => { + const parse = argparse({ + a: { + type: 'boolean', + 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/) + }) + it('works with strings', () => { + const parse = argparse({ + a: { + type: 'string', + positional: true, + }, + }) + expect(parse([]).a).toBe('') + expect(parse(['a']).a).toBe('a') + }) + it('works with multiple positionals', () => { + const parse = argparse({ + a: { + type: 'string', + positional: true, + }, + b: { + type: 'string', + positional: true, + }, + }) + expect(parse(['aaa', 'bbb'])).toEqual({ + a: 'aaa', + b: 'bbb', + }) + }) + it('works amongs regular arguments', () => { + const parse = argparse({ + arg1: { + type: 'string', + }, + arg2: { + type: 'number', + positional: true, + }, + arg3: { + type: 'string', + }, + }) + expect(parse(['--arg1', 'one', '2', '--arg3', 'three'])).toEqual({ + arg1: 'one', + arg2: 2, + arg3: 'three', + }) + }) + }) + it('throws when required args missing', () => { expect(() => argparse({ one: { diff --git a/packages/argparse/src/argparse.ts b/packages/argparse/src/argparse.ts index 2011134..1eed3a2 100644 --- a/packages/argparse/src/argparse.ts +++ b/packages/argparse/src/argparse.ts @@ -14,6 +14,7 @@ export interface IArgConfig { description?: string default?: TArgType required?: boolean + positional?: boolean } export interface IArgsConfig { @@ -64,6 +65,7 @@ export const argparse = ( const it = iterate(args) const aliases: Record = {} + const positional: string[] = [] const requiredArgs = Object.keys(config).reduce((obj, arg) => { const argConfig = config[arg] result[arg] = argConfig.default !== undefined @@ -75,6 +77,9 @@ export const argparse = ( 'Duplicate alias: ' + argConfig.alias) aliases[argConfig.alias] = arg } + if (argConfig.positional) { + positional.push(arg) + } if (argConfig.required) { obj[arg] = true } @@ -108,25 +113,47 @@ export const argparse = ( return lastArgName } + function getNextPositional(): string { + const p = positional.shift() + assert(!!p, 'No defined positional arguments') + return p! + } + while (it.hasNext()) { const arg = it.next() - assert(arg.substring(0, 1) === '-', 'Arguments must start with -') - const argName: string = processFlags(arg) + const isPositional = arg.substring(0, 1) !== '-' + const argName = !isPositional + ? processFlags(arg) + : getNextPositional() const argConfig = config[argName] assert(!!argConfig, 'Unknown argument: ' + arg) delete requiredArgs[argName] const peek = it.peek() switch (argConfig.type) { case 'string': - assert(it.hasNext(), 'Value of argument must be a string: ' + arg) - result[argName] = it.next() + if (isPositional) { + result[argName] = arg + } else { + assert(it.hasNext(), 'Value of argument must be a string: ' + arg) + result[argName] = it.next() + } continue case 'number': - const num = parseInt(it.next(), 10) + const num = parseInt(isPositional ? arg : it.next(), 10) assert(!isNaN(num), 'Value of argument must be a number: ' + arg) result[argName] = num continue case 'boolean': + if (isPositional) { + if (arg === 'true') { + 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