Skip to content

Instantly share code, notes, and snippets.

@Schniz
Last active July 31, 2019 10:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Schniz/0949ef0c412fa71c495456b8dd2a2640 to your computer and use it in GitHub Desktop.
Save Schniz/0949ef0c412fa71c495456b8dd2a2640 to your computer and use it in GitHub Desktop.
TypeSafe builder pattern for commander
import { Command } from 'commander';
export class Program<ArgumentTypes extends { [key: string]: any }> {
private readonly parseFn: (cmd: Command) => ArgumentTypes;
private readonly command: Command;
constructor(command: Command, parse: (cmd: Command) => ArgumentTypes) {
this.parseFn = parse;
this.command = command;
}
parse(argv: string[]): ArgumentTypes {
return this.parseFn(this.command.parse(argv));
}
}
type If<Condition extends boolean, True, False> = Condition extends true
? True
: False;
interface VariadicArgsOptions<Name extends string = string> {
name: Name;
required: boolean;
}
interface CommandOption<Type> {
parse(str: string): Type;
default: Type;
}
type OptionsFor<X> = { [key in keyof X]: CommandOption<X[key]> };
export class ProgramBuilder<
ArgumentTypes extends { [key: string]: any } = {},
HasVariadic extends boolean = false
> {
options: OptionsFor<ArgumentTypes>;
variadicOpts: If<HasVariadic, VariadicArgsOptions, undefined>;
enhancers: ((cmd: Command) => Command)[] = [];
name: string;
version: string;
constructor(
enhancers: ((cmd: Command) => Command)[],
options: OptionsFor<ArgumentTypes>,
name: string,
version: string,
variadicOpts: If<HasVariadic, VariadicArgsOptions, undefined>,
) {
this.enhancers = enhancers;
this.options = options;
this.name = name;
this.version = version;
this.variadicOpts = variadicOpts;
}
static create(name: string, version: string): ProgramBuilder<{}, false> {
return new ProgramBuilder<{}>([], {}, name, version, undefined);
}
option<ArgumentName extends string, ArgumentType>(
opts: {
name: ArgumentName;
shorthand?: string;
default: ArgumentType;
description: string;
} & (ArgumentType extends boolean
? { parse?: never; argName?: never }
: {
parse(val: string): ArgumentType;
argName?: string;
}),
) {
const optionArgument =
typeof opts.default === 'boolean' ? '' : ` <${opts.argName || 'arg'}>`;
const shorthandDef = opts.shorthand ? `-${opts.shorthand}, ` : '';
const parse = typeof opts.default === 'boolean' ? Boolean : opts.parse;
return new ProgramBuilder<
ArgumentTypes & { [key in ArgumentName]: ArgumentType },
HasVariadic
>(
[
...this.enhancers,
cmd =>
cmd.option(
`${shorthandDef}--${opts.name}${optionArgument}`,
opts.description,
opts.default,
),
],
{
...this.options,
[opts.name]: { default: opts.default, parse },
},
this.name,
this.version,
this.variadicOpts,
);
}
variadic<Name extends string>(
opts: If<HasVariadic, never, VariadicArgsOptions<Name>>,
): ProgramBuilder<ArgumentTypes & { [key in Name]: string[] }, true> {
return new ProgramBuilder<
ArgumentTypes & { [key in Name]: string[] },
true
>(this.enhancers, this.options, this.name, this.version, opts);
}
private createCommand(): Command {
const command = new Command();
const usageVariadic = !this.variadicOpts
? ''
: this.variadicOpts.required
? ` <${this.variadicOpts.name}>`
: ` [${this.variadicOpts.name}]`;
command
.name(this.name)
.version(this.version)
.usage(`[options]${usageVariadic}`);
return this.enhancers.reduce((cmd, fn) => fn(cmd), command);
}
build(): Program<ArgumentTypes> {
return new Program(this.createCommand(), p => {
const result: ArgumentTypes = {} as any;
if (this.variadicOpts) {
result[this.variadicOpts.name] = p.args;
}
for (const [key, option] of Object.entries(this.options)) {
result[key] =
p[key] === undefined ? option.default : option.parse(p[key]);
}
return result;
});
}
}
export const program = ProgramBuilder.create;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment