Skip to content
This repository was archived by the owner on Aug 22, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions src/args.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import {IArgsList} from './parse'

export type ParseFn<T> = (input: string) => T

export type NameOf<T> =
T extends {name: string} ? T['name'] :
T extends Arg<infer N, infer _> ? N :
never;

// eslint-disable-next-line @typescript-eslint/interface-name-prefix
export interface IArg<T = string> {
name: string;
export interface IArg<N extends string, T = string> {
name: N;
description?: string;
required?: boolean;
hidden?: boolean;
Expand All @@ -11,8 +18,8 @@ export interface IArg<T = string> {
options?: string[];
}

export interface ArgBase<T> {
name?: string;
export interface ArgBase<N extends string, T> {
name?: N;
description?: string;
hidden?: boolean;
parse: ParseFn<T>;
Expand All @@ -21,27 +28,26 @@ export interface ArgBase<T> {
options?: string[];
}

export type RequiredArg<T> = ArgBase<T> & {
export type RequiredArg<N extends string, T> = ArgBase<N, T> & {
required: true;
value: T;
}

export type OptionalArg<T> = ArgBase<T> & {
export type OptionalArg<N extends string, T> = ArgBase<N, T> & {
required: false;
value?: T;
}

export type Arg<T> = RequiredArg<T> | OptionalArg<T>
export type Arg<N extends string, T> = RequiredArg<N, T> | OptionalArg<N, T>

export function newArg<T>(arg: IArg<T> & { Parse: ParseFn<T> }): Arg<T>
export function newArg(arg: IArg): Arg<string>
export function newArg(arg: IArg<any>): any {
export function newArg<N extends string, T>(arg: IArg<N, T> & { Parse: ParseFn<T> }): Arg<NameOf<typeof arg>, T>
export function newArg<N extends string>(arg: IArg<N>): Arg<NameOf<typeof arg>, string>
export function newArg(arg: IArg<any, any>): Arg<NameOf<typeof arg>, any> {
return {
parse: (i: string) => i,
...arg,
required: Boolean(arg.required),
}
} as any
}

export interface Output {[name: string]: any}
export type Input = IArg<any>[]
export type Input = IArgsList;
12 changes: 6 additions & 6 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const m = Deps()
export interface ICLIParseErrorOptions {
parse: {
input?: ParserInput;
output?: ParserOutput<any, any>;
output?: ParserOutput<any>;
};
}

Expand All @@ -35,9 +35,9 @@ export class CLIParseError extends CLIError {
}

export class InvalidArgsSpecError extends CLIParseError {
public args: Arg<any>[]
public args: Arg<any, any>[]

constructor({args, parse}: ICLIParseErrorOptions & { args: Arg<any>[] }) {
constructor({args, parse}: ICLIParseErrorOptions & { args: Arg<any, any>[] }) {
let message = 'Invalid argument spec'
const namedArgs = args.filter(a => a.name)
if (namedArgs.length > 0) {
Expand All @@ -50,9 +50,9 @@ export class InvalidArgsSpecError extends CLIParseError {
}

export class RequiredArgsError extends CLIParseError {
public args: Arg<any>[]
public args: Arg<any, any>[]

constructor({args, parse}: ICLIParseErrorOptions & { args: Arg<any>[] }) {
constructor({args, parse}: ICLIParseErrorOptions & { args: Arg<any, any>[] }) {
let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}`
const namedArgs = args.filter(a => a.name)
if (namedArgs.length > 0) {
Expand Down Expand Up @@ -93,7 +93,7 @@ export class FlagInvalidOptionError extends CLIParseError {
}

export class ArgInvalidOptionError extends CLIParseError {
constructor(arg: Arg<any>, input: string) {
constructor(arg: Arg<any, any>, input: string) {
const message = `Expected ${input} to be one of: ${arg.options!.join(', ')}`
super({parse: {}, message})
}
Expand Down
23 changes: 13 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import * as args from './args'
import Deps from './deps'
import * as flags from './flags'
import {OutputArgs, OutputFlags, Parser, ParserOutput as Output} from './parse'
import {
OutputArgs,
OutputFlags,
Parser,
ParserRawInput as Input,
ParserOutput as Output,
} from './parse'
import * as Validate from './validate'
export {args}
export {flags}
Expand All @@ -14,19 +20,16 @@ const m = Deps()
// eslint-disable-next-line node/no-missing-require
.add('validate', () => require('./validate').validate as typeof Validate.validate)

export type Input<TFlags extends flags.Output> = {
flags?: flags.Input<TFlags>;
args?: args.Input;
strict?: boolean;
context?: any;
'--'?: boolean;
}
export type FlagsOf<T extends Input<any>['flags'] | undefined> =
T extends undefined ? undefined :
T extends Input<infer TFlags> ? OutputFlags<TFlags> :
never;

export function parse<TFlags, TArgs extends {[name: string]: string}>(argv: string[], options: Input<TFlags>): Output<TFlags, TArgs> {
export function parse<TInput extends Input<any>>(argv: string[], options: TInput): Output<TInput> {
const input = {
argv,
context: options.context,
args: (options.args || []).map((a: any) => args.newArg(a as any)),
args: (options.args || []).map(a => args.newArg(a)),
'--': options['--'],
flags: {
color: flags.defaultFlags.color,
Expand Down
55 changes: 47 additions & 8 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// tslint:disable interface-over-type-literal

import {Arg} from './args'
import {Arg, IArg, NameOf} from './args'
import Deps from './deps'
import * as Errors from './errors'
import * as Flags from './flags'
import {Metadata} from './metadata'
import * as Util from './util'
import {flags, args} from '.'

// eslint-disable-next-line new-cap
const m = Deps()
Expand All @@ -25,11 +26,44 @@ try {
debug = () => {}
}

export type OutputArgs<T extends ParserInput['args']> = { [P in keyof T]: any }
export type OutputFlags<T extends ParserInput['flags']> = { [P in keyof T]: any }
export type ParserOutput<TFlags extends OutputFlags<any>, TArgs extends OutputArgs<any>> = {
flags: TFlags;
args: TArgs;
type DefaultOf<T> = T extends { default: () => infer Default } ? Default
: T extends { default: infer Default } ? Default
: never;
type RequiredTypeOf<T, N extends string = NameOf<T>> = T extends {
name: N;
parse: (raw: string) => infer R;
}
? {[key in N]: R | DefaultOf<T>}
: {[key in N]: string | DefaultOf<T>};

type TypeOf<T> = T extends {required: true}
? RequiredTypeOf<T>
: Partial<RequiredTypeOf<T>>;

type TypesOf<Args extends IArgsList> = {
[I in keyof Args]: TypeOf<Args[I]>
}[number];

type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
? I
: never;

export type ParserRawInput<TFlags extends flags.Output> = {
flags?: flags.Input<TFlags>;
args?: args.Input;
strict?: boolean;
context?: any;
'--'?: boolean;
};

export type OutputArgs<T extends ParserRawInput<any>['args']> = T extends ParserRawInput<any>['args'] ?
UnionToIntersection<TypesOf<NonNullable<T>>> :
undefined;
export type OutputFlags<T extends ParserInput['flags'] | undefined> = { [P in keyof NonNullable<T>]: any }
export type ParserOutput<T extends ParserRawInput<any>> = {
flags: OutputFlags<T['flags']>;
args: OutputArgs<T['args']>;
argv: string[];
raw: ParsingToken[];
metadata: Metadata;
Expand All @@ -39,16 +73,21 @@ export type ArgToken = { type: 'arg'; input: string }
export type FlagToken = { type: 'flag'; flag: string; input: string }
export type ParsingToken = ArgToken | FlagToken

// eslint-disable-next-line @typescript-eslint/interface-name-prefix
export interface IArgsList extends Array<IArg<any, any>> {
[index: number]: IArg<any, any>;
}

export interface ParserInput {
argv: string[];
flags: Flags.Input<any>;
args: Arg<any>[];
args: Arg<any, any>[];
strict: boolean;
context: any;
'--'?: boolean;
}

export class Parser<T extends ParserInput, TFlags extends OutputFlags<T['flags']>, TArgs extends OutputArgs<T['args']>> {
export class Parser<T extends ParserInput, TFlags extends OutputFlags<T['flags']>, TArgs extends any> {
private readonly argv: string[]

private readonly raw: ParsingToken[] = []
Expand Down
4 changes: 2 additions & 2 deletions src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import {Arg} from './args'
import {InvalidArgsSpecError, RequiredArgsError, RequiredFlagError, UnexpectedArgsError} from './errors'
import {ParserInput, ParserOutput} from './parse'

export function validate(parse: { input: ParserInput; output: ParserOutput<any, any> }) {
export function validate(parse: { input: ParserInput; output: ParserOutput<any> }) {
function validateArgs() {
const maxArgs = parse.input.args.length
if (parse.input.strict && parse.output.argv.length > maxArgs) {
const extras = parse.output.argv.slice(maxArgs)
throw new UnexpectedArgsError({parse, args: extras})
}

const missingRequiredArgs: Arg<any>[] = []
const missingRequiredArgs: Arg<any, any>[] = []
let hasOptional = false

parse.input.args.forEach((arg, index) => {
Expand Down
4 changes: 2 additions & 2 deletions test/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ See more help with --help`)

it('accepts falsy', () => {
const out = parse([], {
args: [{name: 'baz', default: false}],
args: [{name: 'baz' as const, default: false}],
})
expect(out.args).to.deep.include({baz: false})
})
Expand Down Expand Up @@ -578,7 +578,7 @@ See more help with --help`)
describe('arg options', () => {
it('accepts valid option', () => {
const out = parse(['myotheropt'], {
args: [{name: 'foo', options: ['myopt', 'myotheropt']}],
args: [{name: 'foo' as const, options: ['myopt', 'myotheropt']}],
})
expect(out.args.foo).to.equal('myotheropt')
})
Expand Down