f2e-server3
Version:
f2e-server 3.0
158 lines (152 loc) • 5.83 kB
text/typescript
import { exit } from "node:process";
interface Option<R extends string, V> {
name: string;
short: string;
argument: R;
description?: string;
defaultValue?: V;
values?: V[];
}
type PickArgs<T extends string> = T extends `-${string}, --${string} <${infer R}>` ? R : never;
type PickCmd<T extends string, V> = T extends `${string} <${infer F}>` ? Command<{[k in F]: V}> : Command;
export class Command<Args extends object = {}> {
options: Option<string, any>[] = [];
commands = new Map<string, Command>();
name: string;
subname: string | undefined;
_version = '';
_description = '';
constructor (name: string) {
this.name = name
}
version (v: string) {
this._version = v;
return this
}
command <T extends string> (name: T) {
if (this.subname) {
throw new Error('subcommand already defined')
}
const [name1, name2] = name.split(/[\s\t]+/)
const c = new Command(name1)
if (name2) {
if (/^<(.*?)>$/.test(name2)) {
c.subname = name2.slice(1, -1)
} else {
throw new Error('invalid subcommand name')
}
}
this.commands.set(name1, c)
return c as PickCmd<T, any>
}
description (description: string) {
this._description = description
return this
}
option <T extends `-${string}, --${string} <${string}>`, F = string> (
name_all: T, description?: string, defaultValue?: F, values?: readonly F[]
): Command<Args & {[k in PickArgs<T>]: F}> {
type R = PickArgs<T>
const [short, name, arg ] = name_all.split(/[\s\t,]+/)
const o: Option<R, F> = {
name,
short,
argument: arg.slice(1, arg.length - 1) as R,
description,
defaultValue,
values: values && Array.from(values) as F[],
}
const t = this as Command<Args & {[k in PickArgs<T>]: F}>
t.options.push(o)
return t
}
actions: {(options: Args): void | Promise<void>}[] = []
action (ac: {(options: Args): void | Promise<void>}) {
this.actions.push(ac)
return this
}
async parse (argv: string[]) {
const { commands } = this
const result: Record<any, any> = {}
let command = this as unknown as Command
for (let i = 2; i < argv.length; i++) {
const item = argv[i];
const next = argv[i + 1];
switch (item) {
case '-V':
case '--version':
console.log(command._version)
exit(0)
case '-h':
case '--help':
console.log(command.showHelp())
exit(0)
default:
}
if (item.startsWith('-')) {
const op = command.options.find(o => o.short === item || o.name === item)
if (op) {
const type = typeof (op.defaultValue || '')
if (!next.startsWith('-')) {
const value = type === 'string' ? next : JSON.parse(next)
if (op.values && op.values.indexOf(value) === -1) {
console.error(`Invalid value "${value}" for option "${op.name}", expected one of "${op.values}"`)
exit(1)
}
result[op.argument] = value;
i++
} else if (type === 'boolean') {
result[op.argument] = true;
} else {
console.error(`Missing value for option "${op.name}"`)
exit(1)
}
} else {
console.log(`Unknown option "${item}"`)
exit(1)
}
} else {
const cmd = commands.get(item)
if (!cmd) {
console.error(`Unknown command "${item}"`)
exit(1)
} else {
command = cmd
if (command.subname) {
result[command.subname] = next
i++
}
}
}
}
for (let i = 0; i < command.options.length; i++) {
const op = command.options[i]
if (op.defaultValue && !(op.argument in result)) {
result[op.argument] = op.defaultValue
}
}
for (let i = 0; i < command.actions.length; i++) {
command.actions[i](result as any)
}
}
private showHelp() {
const { name, subname, _version, _description } = this
const options = this.options.slice(0)
const commands = [...this.commands.values()]
if (_version) {
options.push({ name: '--version', short: '-V', description: 'Show version number', argument: '' as never })
}
options.push({ name: '--help', short: '-h', description: 'Show help', argument: '' as never })
return [
`Usage: ${name} [options] ${_description || ''}`,
`
Options:
${options.map(option => ` ${option.short}, ${option.name}\t\t${option.description}`).join('\n')}
`,
commands.length > 0 && `
Commands:
${commands.map(cmd => ` ${cmd.name}${subname ? ` <${subname}>` : ''}${cmd.options.length > 0 ? ' [options]' : ' '}\t${cmd._description}`).join('\n')}
`
].filter(l => !!l).join('\n')
}
}