diff --git a/package.json b/package.json index ce9f4ec..52b01fa 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "@rondo/image-upload": "file:packages/image-upload", "@rondo/tasq": "file:packages/tasq", "@rondo/jsonrpc": "file:packages/jsonrpc", - "@rondo/scripts": "file:packages/scripts" + "@rondo/scripts": "file:packages/scripts", + "@rondo/argparse": "file:packages/argparse" }, "devDependencies": { "@types/bcrypt": "^3.0.0", diff --git a/packages/argparse/README.md b/packages/argparse/README.md new file mode 100644 index 0000000..f5a262e --- /dev/null +++ b/packages/argparse/README.md @@ -0,0 +1 @@ +# argparse diff --git a/packages/argparse/jest.config.js b/packages/argparse/jest.config.js new file mode 100644 index 0000000..3a4457a --- /dev/null +++ b/packages/argparse/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + roots: [ + '/src' + ], + transform: { + '^.+\\.tsx?$': 'ts-jest' + }, + testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$', + moduleFileExtensions: [ + 'ts', + 'tsx', + 'js', + 'jsx' + ], + setupFiles: ['/jest.setup.js'], + maxConcurrency: 1, + verbose: false +} diff --git a/packages/argparse/jest.setup.js b/packages/argparse/jest.setup.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/argparse/package.json b/packages/argparse/package.json new file mode 100644 index 0000000..e4ba783 --- /dev/null +++ b/packages/argparse/package.json @@ -0,0 +1,14 @@ +{ + "name": "@rondo/argparse", + "private": true, + "scripts": { + "test": "jest", + "lint": "tslint --project .", + "compile": "tsc", + "clean": "rm -rf lib/" + }, + "dependencies": {}, + "types": "lib/index.d.ts", + "devDependencies": {}, + "module": "lib/index.js" +} diff --git a/packages/argparse/src/argparse.test.ts b/packages/argparse/src/argparse.test.ts new file mode 100644 index 0000000..d71ae9b --- /dev/null +++ b/packages/argparse/src/argparse.test.ts @@ -0,0 +1,37 @@ +import {argparse} from './argparse' + +describe('argparse', () => { + + it('parses args', () => { + const args = argparse(['--one', '1', '--two', '2', '--four'], { + one: { + type: 'string', + required: true, + }, + two: { + type: 'number', + default: 1, + }, + four: { + type: 'boolean', + } + }) + + const one: string = args.one + const two: number = args.two + const four: boolean = args.four + + expect(one).toBe('1') + expect(two).toBe(2) + expect(four).toBe(true) + }) + + it('throws when required args missing', () => { + expect(() => argparse([], { + one: { + type: 'string', + required: true, + }, + })).toThrowError(/missing required/i) + }) +}) diff --git a/packages/argparse/src/argparse.ts b/packages/argparse/src/argparse.ts new file mode 100644 index 0000000..989f0a8 --- /dev/null +++ b/packages/argparse/src/argparse.ts @@ -0,0 +1,103 @@ +export type TArgTypeName = 'string' | 'number' | 'boolean' +export type TArgType = + T extends 'string' + ? string + : T extends 'number' + ? number + : T extends 'boolean' + ? boolean + : never + +export interface IArgConfig { + type: T + default?: TArgType + required?: boolean +} + +export interface IArgsConfig { + [arg: string]: IArgConfig +} + +export type TArgs = { + [k in keyof T]: T[k] extends IArgConfig ? + TArgType : never +} + +const iterate = (arr: T[]) => { + let i = -1 + return { + hasNext() { + return i < arr.length - 1 + }, + next(): T { + return arr[++i] + }, + peek(): T { + return arr[i + 1] + } + } +} + +function assert(cond: boolean, message: string) { + if (!cond) { + throw new Error('Error parsing arguments: ' + message) + } +} + +export function argparse( + args: string[], + config: T extends IArgsConfig ? T : never, +): TArgs { + const result = {} as TArgs + const it = iterate(args) + + const usedArgs: Record = {} + const requiredArgs = Object.keys(config).reduce((obj, arg) => { + const argConfig = config[arg] + if (argConfig.default !== undefined) { + result[arg] = argConfig.default + } + obj[arg] = !!argConfig.required + return obj + }, {} as Record) + + while(it.hasNext()) { + const arg = it.next() + assert(arg.substring(0, 2) === '--', 'Arguments must start with --') + const argName = arg.substring(2) + const argConfig = config[argName] + assert(!!argConfig, 'Unknown argument: ' + arg) + delete requiredArgs[argName] + usedArgs[argName] = true + const peek = it.peek() + switch(argConfig.type) { + case 'string': + assert(it.hasNext(), 'Value of argument must be a string: ' + arg) + result[argName] = it.next() + continue + case 'number': + const num = parseInt(it.next(), 10) + assert(!isNaN(num), 'Value of argument must be a number: ' + arg) + result[argName] = num + continue + case 'boolean': + if (peek === 'true') { + it.next() + result[argName] = true + } else if (peek === 'false') { + it.next() + result[argName] = false + } else { + result[argName] = true + } + continue + default: + throw new Error('Unknown type:' + argConfig.type) + } + } + + assert(!Object.keys(requiredArgs).length, 'Missing required args: ' + + Object.keys(requiredArgs).map(r => '--' + r)) + + return result +} diff --git a/packages/argparse/src/index.ts b/packages/argparse/src/index.ts new file mode 100644 index 0000000..0c43f69 --- /dev/null +++ b/packages/argparse/src/index.ts @@ -0,0 +1 @@ +export * from './argparse' diff --git a/packages/argparse/tsconfig.esm.json b/packages/argparse/tsconfig.esm.json new file mode 100644 index 0000000..915284d --- /dev/null +++ b/packages/argparse/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "esm" + }, + "references": [] +} diff --git a/packages/argparse/tsconfig.json b/packages/argparse/tsconfig.json new file mode 100644 index 0000000..94e864b --- /dev/null +++ b/packages/argparse/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.common.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "references": [ + ] +}