From ffb80106da1c15df8881e425dfdf3c9082635ff3 Mon Sep 17 00:00:00 2001 From: XLor Date: Fri, 3 Feb 2023 01:22:30 +0800 Subject: [PATCH] feat!: migrate to new parser --- packages/breadc/src/breadc.ts | 479 +++++++++++---------------- packages/breadc/src/command.ts | 346 ------------------- packages/breadc/src/error.ts | 3 + packages/breadc/src/index.ts | 17 +- packages/breadc/src/logger.ts | 12 +- packages/breadc/src/option.ts | 91 ++--- packages/breadc/src/parser.ts | 378 +++------------------ packages/breadc/src/types.ts | 280 +++------------- packages/breadc/test/breadc.test.ts | 399 +++++++++++----------- packages/breadc/test/command.test.ts | 386 ++++++++++----------- packages/breadc/test/index.test.ts | 290 ---------------- packages/breadc/test/lexer.test.ts | 66 ++++ packages/breadc/test/parse.bench.ts | 30 +- packages/breadc/test/parse.test.ts | 420 +++++++++++++++++++++++ packages/breadc/test/parser.test.ts | 216 ------------ 15 files changed, 1212 insertions(+), 2201 deletions(-) create mode 100644 packages/breadc/src/error.ts delete mode 100644 packages/breadc/test/index.test.ts create mode 100644 packages/breadc/test/lexer.test.ts create mode 100644 packages/breadc/test/parse.test.ts delete mode 100644 packages/breadc/test/parser.test.ts diff --git a/packages/breadc/src/breadc.ts b/packages/breadc/src/breadc.ts index ec8f7451..c96c1ad1 100644 --- a/packages/breadc/src/breadc.ts +++ b/packages/breadc/src/breadc.ts @@ -1,305 +1,208 @@ -import minimist from 'minimist'; - -import type { AppOption, ExtractOption, Logger, ParseResult } from './types'; - -import { twoColumn } from './utils'; -import { createDefaultLogger } from './logger'; -import { Option, OptionConfig } from './option'; -import { Command, CommandConfig, VersionCommand, HelpCommand } from './command'; - -export class Breadc { - private readonly name: string; - private readonly _version: string; - private readonly description?: string | string[]; - - readonly logger: Logger; - - private readonly options: Option[] = []; - private readonly commands: Command[] = []; - private defaultCommand?: Command; - - constructor(name: string, option: AppOption) { - this.name = name; - this._version = option.version ?? 'unknown'; - this.description = option.description; - this.logger = createDefaultLogger(name, option.logger); - - this.commands.push( - new VersionCommand(this.version(), this.logger), - new HelpCommand(this.commands, this.help.bind(this), this.logger) - ); - } - - version() { - return `${this.name}/${this._version}`; - } - - help(commands: Command[] = []) { - const output: string[] = []; - const println = (msg: string) => output.push(msg); - - println(this.version()); - if (commands.length === 0) { - if (this.description) { - println(''); - if (Array.isArray(this.description)) { - for (const line of this.description) { - println(line); - } - } else { - println(this.description); - } - } - - if (this.defaultCommand) { - println(``); - println(`Usage:`); - println(` $ ${this.name} ${this.defaultCommand.format}`); - } - } else if (commands.length === 1) { - const command = commands[0]; - if (command.description) { - println(''); - println(command.description); - } - - println(``); - println(`Usage:`); - println(` $ ${this.name} ${command.format}`); - } - - if (commands.length !== 1) { - const cmdList = (commands.length === 0 ? this.commands : commands).filter( - (c) => !c.isInternal - ); - - println(``); - println(`Commands:`); - const commandHelps = cmdList.map( - (c) => - [` $ ${this.name} ${c.format}`, c.description] as [string, string] - ); - - for (const line of twoColumn(commandHelps)) { - println(line); - } - } - - println(``); - println(`Options:`); - const optionHelps = ([] as Array<[string, string]>).concat([ - ...(commands.length > 0 - ? commands.flatMap((cmd) => - cmd.options.map( - (o) => [` ${o.format}`, o.description] as [string, string] - ) - ) - : []), - ...this.options.map( - (o) => [` ${o.format}`, o.description] as [string, string] - ), - [` -h, --help`, `Display this message`], - [` -v, --version`, `Display version number`] - ]); - - for (const line of twoColumn(optionHelps)) { - println(line); - } - - println(``); - - return output; - } - - option( - format: F, - description: string, - config?: Omit, 'description'> - ): Breadc>; - - option( - format: F, - config?: OptionConfig - ): Breadc>; - - option( - format: F, - configOrDescription: OptionConfig | string = '', - otherConfig: Omit, 'description'> = {} - ): Breadc> { - const config: OptionConfig = - typeof configOrDescription === 'object' - ? configOrDescription - : { ...otherConfig, description: configOrDescription }; - - try { - const option = new Option(format, config); - this.options.push(option as unknown as Option); - } catch (error: any) { - this.logger.warn(error.message); - } - return this as Breadc>; - } - - command( - format: F, - description: string, - config?: Omit - ): Command; - - command( - format: F, - config?: CommandConfig - ): Command; - - command( - format: F, - configOrDescription: CommandConfig | string = '', - otherConfig: Omit = {} - ): Command { - const config: CommandConfig = - typeof configOrDescription === 'object' - ? configOrDescription - : { ...otherConfig, description: configOrDescription }; - - const command = new Command(format, { ...config, logger: this.logger }); - if (command.default) { - if (this.defaultCommand) { - this.logger.warn('You can not have two default commands.'); +import type { Breadc, AppOption, Command, Argument, Option } from './types'; + +import { ParseError } from './error'; +import { makeOption } from './option'; +import { Context, makeTreeNode, parse } from './parser'; + +export function breadc(name: string, config: AppOption = {}) { + const allCommands: Command[] = []; + const globalOptions: Option[] = []; + + const initContextOptions = (options: Option[], context: Context) => { + for (const option of options) { + const defaultValue = + option.type === 'boolean' + ? false + : option.type === 'string' + ? option.default ?? '' + : false; + context.options.set(option.name, option); + if (option.short) { + context.options.set(option.short, option); } - this.defaultCommand = command; + context.result.options[option.name] = defaultValue; } - this.commands.push(command); - return command as Command; - } - - parse(args: string[]): ParseResult { - const allowOptions: Option[] = [ - ...this.options, - ...this.commands.flatMap((c) => c.options) - ]; + }; - { - // Check option names conflict - const names = new Map(); - for (const option of allowOptions) { - if (names.has(option.name)) { - const otherOption = names.get(option.name)!; - if (otherOption.type !== option.type) { - this.logger.warn(`Option "${option.name}" encounters conflict`); + const root = makeTreeNode({ + init(context) { + initContextOptions(globalOptions, context); + }, + finish() {} + }); + + const breadc: Breadc = { + option(text): Breadc { + const option = makeOption(text); + globalOptions.push(option); + return breadc; + }, + command(text): Command { + let cursor = root; + + const args: Argument[] = []; + const options: Option[] = []; + + const command: Command = { + callback: undefined, + description: '', + arguments: args, + option(text) { + const option = makeOption(text); + options.push(option); + return command; + }, + action(fn) { + command.callback = fn; + if (cursor === root) { + globalOptions.push(...options); } - } else { - names.set(option.name, option); + return breadc; } - } - } + }; + + const node = makeTreeNode({ + command, + init(context) { + initContextOptions(options, context); + }, + finish(context) { + const rest = context.result['--']; + for (let i = 0; i < args.length; i++) { + if (args[i].type === 'const') { + if (rest[i] !== args[i].name) { + throw new ParseError(`Internal`); + } + } else if (args[i].type === 'require') { + if (i >= rest.length) { + throw new ParseError(`You must provide require argument`); + } + context.result.arguments.push(rest[i]); + } else if (args[i].type === 'optional') { + context.result.arguments.push(rest[i]); + } else if (args[i].type === 'rest') { + context.result.arguments.push(rest.splice(i)); + } + } + context.result['--'] = rest.splice(args.length); + } + }); - const alias = allowOptions.reduce( - (map: Record, o) => { - if (o.shortcut) { - map[o.shortcut] = o.name; + { + // 0 -> aaa bbb + // 1 -> aaa bbb + // 2 -> aaa bbb [zzz] + // 3 -> bbb bbb [...www] + let state = 0; + for (let i = 0; i < text.length; i++) { + if (text[i] === '<') { + if (state !== 0 && state !== 1) { + // error here + } + + const start = i; + while (i < text.length && text[i] !== '>') { + i++; + } + + const name = text.slice(start + 1, i); + state = 1; + args.push({ type: 'require', name }); + } else if (text[i] === '[') { + if (state !== 0 && state !== 1) { + // error here + } + + const start = i; + while (i < text.length && text[i] !== ']') { + i++; + } + + const name = text.slice(start + 1, i); + state = 2; + if (name.startsWith('...')) { + args.push({ type: 'rest', name }); + } else { + args.push({ type: 'optional', name }); + } + } else if (text[i] !== ' ') { + if (state !== 0) { + // error here + } + + const start = i; + while (i < text.length && text[i] !== ' ') { + i++; + } + const name = text.slice(start, i); + + if (cursor.children.has(name)) { + cursor = cursor.children.get(name)!; + // console.log(text); + // console.log(name); + // console.log(cursor); + } else { + const internalNode = makeTreeNode({ + next(token, context) { + const t = token.raw(); + context.result['--'].push(t); + if (internalNode.children.has(t)) { + const next = internalNode.children.get(t)!; + next.init(context); + return next; + } else { + throw new ParseError(`Unknown sub-command ${t}`); + } + }, + finish() { + throw new ParseError(`Unknown sub-command`); + } + }); + + cursor.children.set(name, internalNode); + cursor = internalNode; + } + + state = 0; + args.push({ type: 'const', name }); + } } - return map; - }, - { h: 'help', v: 'version' } - ); - const defaultValue = allowOptions - .filter( - (o) => - o.type === 'boolean' && - o.default !== undefined && - o.default !== null && - typeof o.default === 'boolean' - ) - .reduce((map: Record, o) => { - map[o.name] = o.default; - return map; - }, {}); - const argv = minimist(args, { - string: allowOptions - .filter((o) => o.type === 'string') - .map((o) => o.name), - boolean: allowOptions - .filter((o) => o.type === 'boolean') - .map((o) => o.name) - .concat(['help', 'version']), - default: defaultValue, - alias, - '--': true, - unknown: (t) => { - if (t[0] !== '-') return true; - else { - if (['--help', '-h', '--version', '-v'].includes(t)) { - return true; - } else { - this.logger.warn(`Find unknown flag "${t}"`); - return false; + cursor.command = command; + if (cursor !== root) { + for (const [key, value] of cursor.children) { + node.children.set(key, value); } + cursor.children = node.children; + cursor.next = node.next; + cursor.init = node.init; + cursor.finish = node.finish; + } else { + cursor.finish = node.finish; } } - }); - for (const shortcut of Object.keys(alias)) { - delete argv[shortcut]; - } - - // Try non-default command first - for (const command of this.commands) { - if (!command.default && command.shouldRun(argv)) { - return command.parseArgs(argv, this.options); + allCommands.push(command); + + return command; + }, + parse(args: string[]) { + return parse(root, args); + }, + async run(args: string[]) { + const result = parse(root, args); + const command = result.command; + if (command) { + if (command.callback) { + return command.callback(...result.arguments, { + ...result.options, + '--': result['--'] + }); + } } + return undefined as any; } - // Then try default command - if (this.defaultCommand) { - // Fix sideEffect - this.defaultCommand.shouldRun(argv); - return this.defaultCommand.parseArgs(argv, this.options); - } - - const argumentss = argv['_']; - const options: Record = argv; - delete options['_']; - delete options['--']; - delete options['help']; - delete options['version']; - - return { - command: undefined, - arguments: argumentss, - options, - '--': [] - }; - } - - private readonly callbacks = { - pre: [] as Array<(option: GlobalOption) => void | Promise>, - post: [] as Array<(option: GlobalOption) => void | Promise> }; - on( - event: 'pre' | 'post', - fn: (option: GlobalOption) => void | Promise - ) { - this.callbacks[event].push(fn); - } - - async run(args: string[]): Promise { - const parsed = this.parse(args); - if (parsed.command) { - await Promise.all( - this.callbacks.pre.map((fn) => fn(parsed.options as any)) - ); - const returnValue = await parsed.command.run(...parsed.arguments, { - '--': parsed['--'], - ...parsed.options - }); - await Promise.all( - this.callbacks.post.map((fn) => fn(parsed.options as any)) - ); - return returnValue; - } else { - return undefined; - } - } + return breadc; } diff --git a/packages/breadc/src/command.ts b/packages/breadc/src/command.ts index 5b98f057..e69de29b 100644 --- a/packages/breadc/src/command.ts +++ b/packages/breadc/src/command.ts @@ -1,346 +0,0 @@ -import type { ParsedArgs } from 'minimist'; - -import * as kolorist from 'kolorist'; - -import type { - ActionFn, - ExtractCommand, - ExtractOption, - Logger, - ParseResult -} from './types'; - -import { Option, OptionConfig } from './option'; - -export interface CommandConfig { - description?: string; -} - -export class Command< - F extends string = string, - CommandOption extends object = {} -> { - protected static MaxDep = 5; - - protected readonly logger: Logger; - - readonly format: string; - readonly description: string; - - readonly prefix: string[][]; - readonly arguments: string[]; - readonly default: boolean; - readonly options: Option[] = []; - - private actionFn?: ActionFn, CommandOption>; - - constructor(format: F, config: CommandConfig & { logger: Logger }) { - this.format = format; - - const pieces = format - .split(' ') - .map((t) => t.trim()) - .filter(Boolean); - const prefix = pieces.filter((p) => !isArg(p)); - this.default = prefix.length === 0; - this.prefix = this.default ? [] : [prefix]; - this.arguments = pieces.filter(isArg); - - this.description = config.description ?? ''; - this.logger = config.logger; - - { - const restArgs = this.arguments.findIndex((a) => a.startsWith('[...')); - if (restArgs !== -1 && restArgs !== this.arguments.length - 1) { - this.logger.warn( - `Expand arguments ${this.arguments[restArgs]} should be placed at the last position` - ); - } - if (pieces.length > Command.MaxDep) { - this.logger.warn(`Command format string "${format}" is too long`); - } - } - } - - get isInternal(): boolean { - return this instanceof InternalCommand; - } - - alias(command: string) { - const pieces = command - .split(' ') - .map((t) => t.trim()) - .filter(Boolean); - this.prefix.push(pieces); - return this; - } - - option( - format: OF, - description: string, - config?: Omit, 'description'> - ): Command>; - - option( - format: OF, - config?: OptionConfig - ): Command>; - - option( - format: OF, - configOrDescription: OptionConfig | string = '', - otherConfig: Omit, 'description'> = {} - ): Command> { - const config: OptionConfig = - typeof configOrDescription === 'object' - ? configOrDescription - : { ...otherConfig, description: configOrDescription }; - - try { - const option = new Option(format, config); - this.options.push(option as unknown as Option); - } catch (error: any) { - this.logger.warn(error.message); - } - return this as Command>; - } - - hasPrefix(parsedArgs: ParsedArgs) { - const argv = parsedArgs['_']; - if (argv.length === 0) { - return this.default; - } else { - for (const prefix of this.prefix) { - if (prefix.length > 0 && prefix[0] === argv[0]) { - return true; - } - } - return false; - } - } - - shouldRun(parsedArgs: ParsedArgs) { - const args = parsedArgs['_']; - for (const prefix of this.prefix) { - let match = true; - for (let i = 0; match && i < prefix.length; i++) { - if (args[i] !== prefix[i]) { - match = false; - } - } - if (match) { - // SideEffect: remove args prefix - args.splice(0, prefix.length); - return true; - } - } - if (this.default) return true; - return false; - } - - parseArgs(argv: ParsedArgs, globalOptions: Option[]): ParseResult { - const pieces = argv['_']; - const args: any[] = []; - const restArgs: any[] = []; - - for (let i = 0, used = 0; i <= this.arguments.length; i++) { - if (i === this.arguments.length) { - // Pass the rest arguments - restArgs.push(...pieces.slice(used).map(String)); - restArgs.push(...(argv['--'] ?? []).map(String)); - } else if (i < pieces.length) { - if (this.arguments[i].startsWith('[...')) { - args.push(pieces.slice(i).map(String)); - used = pieces.length; - } else { - args.push(String(pieces[i])); - used++; - } - } else { - if (this.arguments[i].startsWith('<')) { - this.logger.warn( - `You should provide the argument "${this.arguments[i]}"` - ); - args.push(''); - } else if (this.arguments[i].startsWith('[...')) { - args.push([]); - } else if (this.arguments[i].startsWith('[')) { - args.push(undefined); - } else { - this.logger.warn(`unknown format string ("${this.arguments[i]}")`); - } - } - } - - const fullOptions = globalOptions.concat(this.options).reduce((map, o) => { - map.set(o.name, o); - return map; - }, new Map()); - const options: Record = argv; - delete options['_']; - - for (const [name, rawOption] of fullOptions) { - if (rawOption.type === 'boolean') continue; - - if (rawOption.required) { - if (options[name] === undefined) { - options[name] = false; - } else if (options[name] === '') { - options[name] = true; - } - } else { - if (options[name] === false) { - options[name] = undefined; - } else if (!(name in options)) { - options[name] = undefined; - } - } - - if (rawOption.construct !== undefined) { - // @ts-ignore - options[name] = rawOption.construct(options[name]); - } else if (rawOption.default !== undefined) { - if ( - options[name] === undefined || - options[name] === false || - options[name] === '' - ) { - options[name] = rawOption.default; - } - } - } - for (const key of Object.keys(options)) { - if (!fullOptions.has(key)) { - delete options[key]; - } - } - - return { - // @ts-ignore - command: this, - arguments: args, - options, - '--': restArgs - }; - } - - action(fn: ActionFn, CommandOption>) { - this.actionFn = fn; - } - - async run(...args: any[]) { - if (this.actionFn) { - // @ts-ignore - return await this.actionFn(...args, { - logger: this.logger, - color: kolorist - }); - } else { - this.logger.warn( - `You may miss action function in ${ - this.format ? `"${this.format}"` : '' - }` - ); - return undefined; - } - } -} - -class InternalCommand extends Command { - hasPrefix(_args: ParsedArgs): boolean { - return false; - } - - parseArgs(args: ParsedArgs, _globalOptions: Option[]): ParseResult { - const argumentss: any[] = args['_']; - const options: Record = args; - delete options['_']; - delete options['help']; - delete options['version']; - - return { - // @ts-ignore - command: this, - arguments: argumentss, - options: args, - '--': [] - }; - } -} - -type HelpFn = (commands: Command[]) => string[]; - -export class HelpCommand extends InternalCommand { - private readonly commands: Command[]; - private readonly help: HelpFn; - - private readonly runCommands: Command[] = []; - private readonly helpCommands: Command[] = []; - - constructor(commands: Command[], help: HelpFn, logger: Logger) { - super('-h, --help', { description: 'Display this message', logger }); - this.commands = commands; - this.help = help; - } - - shouldRun(args: ParsedArgs) { - const isRestEmpty = !args['--']?.length; - if ((args.help || args.h) && isRestEmpty) { - if (args['_'].length > 0) { - for (const cmd of this.commands) { - if (!cmd.default && !cmd.isInternal) { - if (cmd.shouldRun(args)) { - this.runCommands.push(cmd); - } else if (cmd.hasPrefix(args)) { - this.helpCommands.push(cmd); - } - } - } - } - return true; - } else { - return false; - } - } - - async run() { - const shouldHelp = - this.runCommands.length > 0 ? this.runCommands : this.helpCommands; - for (const line of this.help(shouldHelp)) { - this.logger.println(line); - } - this.runCommands.splice(0); - this.helpCommands.splice(0); - } -} - -export class VersionCommand extends InternalCommand { - private readonly version: string; - - constructor(version: string, logger: Logger) { - super('-v, --version', { description: 'Display version number', logger }); - this.version = version; - } - - shouldRun(args: ParsedArgs) { - const isEmpty = !args['_'].length && !args['--']?.length; - if (args.version && isEmpty) { - return true; - } else if (args.v && isEmpty) { - return true; - } else { - return false; - } - } - - async run() { - this.logger.println(this.version); - } -} - -function isArg(arg: string) { - return ( - (arg[0] === '[' && arg[arg.length - 1] === ']') || - (arg[0] === '<' && arg[arg.length - 1] === '>') - ); -} diff --git a/packages/breadc/src/error.ts b/packages/breadc/src/error.ts new file mode 100644 index 00000000..8b062b24 --- /dev/null +++ b/packages/breadc/src/error.ts @@ -0,0 +1,3 @@ +export class BreadcError extends Error {} + +export class ParseError extends Error {} diff --git a/packages/breadc/src/index.ts b/packages/breadc/src/index.ts index 8c36ba10..e584b7d1 100644 --- a/packages/breadc/src/index.ts +++ b/packages/breadc/src/index.ts @@ -1,16 +1,5 @@ -import type { AppOption } from './types'; +import { breadc } from './breadc'; -import { Breadc } from './breadc'; +export type { AppOption, Breadc, Command, Option, Argument } from './types'; -export type { Breadc }; - -export type { Command, CommandConfig } from './command'; - -export type { Option, OptionConfig } from './option'; - -export default function breadc( - name: string, - option: AppOption = {} -): Breadc { - return new Breadc(name, option); -} +export default breadc; diff --git a/packages/breadc/src/logger.ts b/packages/breadc/src/logger.ts index ced5be12..ba875116 100644 --- a/packages/breadc/src/logger.ts +++ b/packages/breadc/src/logger.ts @@ -1,7 +1,15 @@ -import type { Logger, LoggerFn } from './types'; - import { blue, gray, red, yellow } from 'kolorist'; +export type LoggerFn = (message: string, ...args: any[]) => void; + +export interface Logger { + println: LoggerFn; + info: LoggerFn; + warn: LoggerFn; + error: LoggerFn; + debug: LoggerFn; +} + export function createDefaultLogger( name: string, logger?: Partial | LoggerFn diff --git a/packages/breadc/src/option.ts b/packages/breadc/src/option.ts index c126b67a..63b63439 100644 --- a/packages/breadc/src/option.ts +++ b/packages/breadc/src/option.ts @@ -1,74 +1,37 @@ -import type { ExtractOptionType } from './types'; +import type { Option } from './types'; -export interface OptionConfig< - F extends string, - T = never, - O = ExtractOptionType -> { - /** - * Option description - */ - description?: string; +import { BreadcError } from './error'; - /** - * Option string default value - */ - default?: O extends boolean ? boolean : string; +// TODO: support --no-xxx - /** - * Transform option text - */ - construct?: (rawText: ExtractOptionType) => T; -} - -/** - * Option - * - * Option format must follow: - * + --option - * + -o, --option - * + --option - * + --option [arg] - */ -export class Option< - F extends string = string, - T = string, - O = ExtractOptionType -> { - private static OptionRE = - /^(-[a-zA-Z0-9], )?--([a-zA-Z0-9\-]+)( \[[a-zA-Z0-9]+\]| <[a-zA-Z0-9]+>)?$/; - - readonly name: string; - readonly shortcut?: string; - readonly default?: O extends boolean ? boolean : string; - readonly format: string; - readonly description: string; - readonly type: 'string' | 'boolean'; - readonly required: boolean; +const OptionRE = + /^(-[a-zA-Z0-9], )?--([a-zA-Z0-9\-]+)( \[...[a-zA-Z0-9]+\]| <[a-zA-Z0-9]+>)?$/; - readonly construct?: (rawText: ExtractOptionType) => T; +export function makeOption(format: F): Option { + let type: 'string' | 'boolean' = 'string'; + let name = ''; + let short = undefined; - constructor(format: F, config: OptionConfig = {}) { - this.format = format; - - const match = Option.OptionRE.exec(format); - if (match) { - if (match[3]) { - this.type = 'string'; - } else { - this.type = 'boolean'; - } - this.name = match[2]; - if (match[1]) { - this.shortcut = match[1][1]; - } + const match = OptionRE.exec(format); + if (match) { + if (match[3]) { + type = 'string'; } else { - throw new Error(`Can not parse option format from "${format}"`); + type = 'boolean'; + } + name = match[2]; + if (match[1]) { + short = match[1][1]; } - this.description = config.description ?? ''; - this.required = format.indexOf('<') !== -1; - this.default = config.default; - this.construct = config.construct; + return { + format, + type, + name, + short, + description: '' + }; + } else { + throw new BreadcError(`Can not parse option format from "${format}"`); } } diff --git a/packages/breadc/src/parser.ts b/packages/breadc/src/parser.ts index 5be87c6e..1979935d 100644 --- a/packages/breadc/src/parser.ts +++ b/packages/breadc/src/parser.ts @@ -1,41 +1,6 @@ -export class Lexer { - private readonly rawArgs: string[]; +import type { ParseResult, Command, Option } from './types'; - private cursor: number = 0; - - constructor(rawArgs: string[]) { - this.rawArgs = rawArgs; - } - - public next(): Token | undefined { - const value = this.rawArgs[this.cursor]; - this.cursor += 1; - return value ? new Token(value) : undefined; - } - - public hasNext(): boolean { - return this.cursor + 1 < this.rawArgs.length; - } - - public peek(): Token | undefined { - const value = this.rawArgs[this.cursor]; - return value ? new Token(value) : undefined; - } - - [Symbol.iterator](): Iterator { - const that = this; - return { - next() { - const value = that.rawArgs[that.cursor]; - that.cursor += 1; - return { - value: value ? new Token(value) : undefined, - done: that.cursor > that.rawArgs.length - } as IteratorYieldResult | IteratorReturnResult; - } - }; - } -} +import { ParseError } from './error'; export type TokenType = '--' | '-' | 'number' | 'string' | 'long' | 'short'; @@ -96,23 +61,54 @@ export class Token { } } -class BreadcError extends Error {} +export class Lexer { + private readonly rawArgs: string[]; + + private cursor: number = 0; + + constructor(rawArgs: string[]) { + this.rawArgs = rawArgs; + } + + public next(): Token | undefined { + const value = this.rawArgs[this.cursor]; + this.cursor += 1; + return value ? new Token(value) : undefined; + } + + public hasNext(): boolean { + return this.cursor + 1 < this.rawArgs.length; + } -class ParseError extends Error {} + public peek(): Token | undefined { + const value = this.rawArgs[this.cursor]; + return value ? new Token(value) : undefined; + } -interface Context { + [Symbol.iterator](): Iterator { + const that = this; + return { + next() { + const value = that.rawArgs[that.cursor]; + that.cursor += 1; + return { + value: value ? new Token(value) : undefined, + done: that.cursor > that.rawArgs.length + } as IteratorYieldResult | IteratorReturnResult; + } + }; + } +} + +export interface Context { lexer: Lexer; options: Map; - result: { - arguments: any[]; - options: Record; - '--': string[]; - }; + result: ParseResult; } -interface TreeNode { +export interface TreeNode { command?: Command; children: Map; @@ -124,7 +120,7 @@ interface TreeNode { finish(context: Context): void; } -function makeTreeNode(pnode: Partial): TreeNode { +export function makeTreeNode(pnode: Partial): TreeNode { const node: TreeNode = { children: new Map(), init() {}, @@ -145,38 +141,6 @@ function makeTreeNode(pnode: Partial): TreeNode { return node; } -function makeOption(format: F): Option { - const OptionRE = - /^(-[a-zA-Z0-9], )?--([a-zA-Z0-9\-]+)( \[...[a-zA-Z0-9]+\]| <[a-zA-Z0-9]+>)?$/; - - let type: 'string' | 'boolean' = 'string'; - let name = ''; - let short = undefined; - - const match = OptionRE.exec(format); - if (match) { - if (match[3]) { - type = 'string'; - } else { - type = 'boolean'; - } - name = match[2]; - if (match[1]) { - short = match[1][1]; - } - - return { - format, - type, - name, - short, - description: '' - }; - } else { - throw new BreadcError(`Can not parse option format from "${format}"`); - } -} - export function parse(root: TreeNode, args: string[]) { const lexer = new Lexer(args); const context: Context = { @@ -234,263 +198,9 @@ export function parse(root: TreeNode, args: string[]) { } return { - node: cursor, + command: cursor.command, arguments: context.result.arguments, options: context.result.options, '--': context.result['--'] }; } - -export function breadc( - name: string, - config: { - version?: string; - description?: string | string[]; - } = {} -) { - const allCommands: Command[] = []; - const globalOptions: Option[] = []; - - const initContextOptions = (options: Option[], context: Context) => { - for (const option of options) { - const defaultValue = - option.type === 'boolean' - ? false - : option.type === 'string' - ? option.default ?? '' - : undefined; - context.options.set(option.name, option); - if (option.short) { - context.options.set(option.short, option); - } - context.result.options[option.name] = defaultValue; - } - }; - - const root = makeTreeNode({ - init(context) { - initContextOptions(globalOptions, context); - }, - finish() {} - }); - - const breadc: Breadc = { - option(text): Breadc { - const option = makeOption(text); - globalOptions.push(option); - return breadc; - }, - command(text): Command { - let cursor = root; - - const args: Argument[] = []; - const options: Option[] = []; - - const command: Command = { - callback: undefined, - description: '', - arguments: args, - option(text) { - const option = makeOption(text); - options.push(option); - return command; - }, - action(fn) { - command.callback = fn; - if (cursor === root) { - globalOptions.push(...options); - } - return breadc; - } - }; - - const node = makeTreeNode({ - command, - init(context) { - initContextOptions(options, context); - }, - finish(context) { - const rest = context.result['--']; - for (let i = 0; i < args.length; i++) { - if (args[i].type === 'const') { - if (rest[i] !== args[i].name) { - throw new ParseError(`Internal`); - } - } else if (args[i].type === 'require') { - if (i >= rest.length) { - throw new ParseError(`You must provide require argument`); - } - context.result.arguments.push(rest[i]); - } else if (args[i].type === 'optional') { - context.result.arguments.push(rest[i]); - } else if (args[i].type === 'rest') { - context.result.arguments.push(rest.splice(i)); - } - } - context.result['--'] = rest.splice(args.length); - } - }); - - { - // 0 -> aaa bbb - // 1 -> aaa bbb - // 2 -> aaa bbb [zzz] - // 3 -> bbb bbb [...www] - let state = 0; - for (let i = 0; i < text.length; i++) { - if (text[i] === '<') { - if (state !== 0 && state !== 1) { - // error here - } - - const start = i; - while (i < text.length && text[i] !== '>') { - i++; - } - - const name = text.slice(start + 1, i); - state = 1; - args.push({ type: 'require', name }); - } else if (text[i] === '[') { - if (state !== 0 && state !== 1) { - // error here - } - - const start = i; - while (i < text.length && text[i] !== ']') { - i++; - } - - const name = text.slice(start + 1, i); - state = 2; - if (name.startsWith('...')) { - args.push({ type: 'rest', name }); - } else { - args.push({ type: 'optional', name }); - } - } else if (text[i] !== ' ') { - if (state !== 0) { - // error here - } - - const start = i; - while (i < text.length && text[i] !== ' ') { - i++; - } - const name = text.slice(start, i); - - if (cursor.children.has(name)) { - cursor = cursor.children.get(name)!; - // console.log(text); - // console.log(name); - // console.log(cursor); - } else { - const internalNode = makeTreeNode({ - next(token, context) { - const t = token.raw(); - context.result['--'].push(t); - if (internalNode.children.has(t)) { - const next = internalNode.children.get(t)!; - next.init(context); - return next; - } else { - throw new ParseError(`Unknown sub-command ${t}`); - } - }, - finish() { - throw new ParseError(`Unknown sub-command`); - } - }); - - cursor.children.set(name, internalNode); - cursor = internalNode; - } - - state = 0; - args.push({ type: 'const', name }); - } - } - - cursor.command = command; - if (cursor !== root) { - for (const [key, value] of cursor.children) { - node.children.set(key, value); - } - cursor.children = node.children; - cursor.next = node.next; - cursor.init = node.init; - cursor.finish = node.finish; - } else { - cursor.finish = node.finish; - } - } - - allCommands.push(command); - - return command; - }, - parse(args: string[]) { - return parse(root, args); - }, - async run(args: string[]) { - const result = parse(root, args); - const command = result.node.command; - if (command) { - if (command.callback) { - return command.callback(...result.arguments, { - ...result.options, - '--': result['--'] - }); - } - } - return undefined as any; - } - }; - - return breadc; -} - -type ActionFn = (...args: any[]) => any; - -interface Breadc { - option( - text: string, - option?: { description?: string; default?: string } - ): Breadc; - - command(text: string, option?: { description?: string }): Command; - - parse(args: string[]): any; - - run(args: string[]): Promise; -} - -interface Command { - callback?: ActionFn; - - description: string; - - arguments: Argument[]; - - option( - text: string, - option?: { description?: string; default?: string } - ): Command; - - action(fn: ActionFn): Breadc; -} - -interface Option { - format: F; - name: string; - short?: string; - type: 'boolean' | 'string'; - default?: T; - description: string; -} - -type Argument = - | { type: 'const'; name: string } - | { type: 'require'; name: string } - | { type: 'optional'; name: string } - | { type: 'rest'; name: string }; diff --git a/packages/breadc/src/types.ts b/packages/breadc/src/types.ts index 1dbcbae5..aa8e1e27 100644 --- a/packages/breadc/src/types.ts +++ b/packages/breadc/src/types.ts @@ -1,247 +1,59 @@ -import kolorist from 'kolorist'; - -import type { Command } from './command'; - export interface AppOption { version?: string; description?: string | string[]; - help?: string | string[] | (() => string | string[]); + // help?: string | string[] | (() => string | string[]); - logger?: Partial | LoggerFn; + // logger?: Partial | LoggerFn; } -export type LoggerFn = (message: string, ...args: any[]) => void; - -export interface Logger { - println: LoggerFn; - info: LoggerFn; - warn: LoggerFn; - error: LoggerFn; - debug: LoggerFn; -} +export type ActionFn = (...args: any[]) => any; export interface ParseResult { - command: Command | undefined; - arguments: any[]; - options: Record; + arguments: Array; + options: Record; '--': string[]; } -export type ExtractOption = { - [k in ExtractOptionName]: D extends undefined ? ExtractOptionType : D; -}; - -/** - * Extract option name type - * - * Examples: - * + const t1: ExtractOption<'--option' | '--hello'> = 'hello' - * + const t2: ExtractOption<'-r, --root'> = 'root' - */ -export type ExtractOptionName = - T extends `-${Letter}, --${infer R} [${infer U}]` - ? R - : T extends `-${Letter}, --${infer R} <${infer U}>` - ? R - : T extends `-${Letter}, --${infer R}` - ? R - : T extends `--${infer R} [${infer U}]` - ? R - : T extends `--${infer R} <${infer U}>` - ? R - : T extends `--${infer R}` - ? R - : never; - -export type ExtractOptionType = - T extends `-${Letter}, --${infer R} [${infer U}]` - ? string | undefined - : T extends `-${Letter}, --${infer R} <${infer U}>` - ? string | boolean - : T extends `-${Letter}, --${infer R}` - ? boolean - : T extends `--${infer R} [${infer U}]` - ? string | undefined - : T extends `--${infer R} <${infer U}>` - ? string | boolean - : T extends `--${infer R}` - ? boolean - : never; - -type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; - -type Lowercase = - | 'a' - | 'b' - | 'c' - | 'd' - | 'e' - | 'f' - | 'g' - | 'h' - | 'i' - | 'j' - | 'k' - | 'l' - | 'm' - | 'n' - | 'o' - | 'p' - | 'q' - | 'r' - | 's' - | 't' - | 'u' - | 'v' - | 'w' - | 'x' - | 'y' - | 'z'; - -type Uppercase = - | 'A' - | 'B' - | 'C' - | 'D' - | 'E' - | 'F' - | 'G' - | 'H' - | 'I' - | 'J' - | 'K' - | 'L' - | 'M' - | 'N' - | 'O' - | 'P' - | 'Q' - | 'R' - | 'S' - | 'T' - | 'U' - | 'V' - | 'W' - | 'X' - | 'Y' - | 'Z'; - -type Letter = Lowercase | Uppercase; - -type Push = [...T, U, R]; - -type Context = { logger: Logger; color: typeof kolorist }; - -export type ActionFn = ( - ...arg: Push -) => R | Promise; - -/** - * Max Dep: 5 - * - * Generated by: npx tsx examples/genType.ts 5 - */ -export type ExtractCommand = - T extends `<${infer P1}> <${infer P2}> <${infer P3}> <${infer P4}> [...${infer P5}]` - ? [string, string, string, string, string[]] - : T extends `<${infer P1}> <${infer P2}> <${infer P3}> <${infer P4}> [${infer P5}]` - ? [string, string, string, string, string | undefined] - : T extends `<${infer P1}> <${infer P2}> <${infer P3}> <${infer P4}> <${infer P5}>` - ? [string, string, string, string, string] - : T extends `${infer P1} <${infer P2}> <${infer P3}> <${infer P4}> [...${infer P5}]` - ? [string, string, string, string[]] - : T extends `${infer P1} <${infer P2}> <${infer P3}> <${infer P4}> [${infer P5}]` - ? [string, string, string, string | undefined] - : T extends `${infer P1} <${infer P2}> <${infer P3}> <${infer P4}> <${infer P5}>` - ? [string, string, string, string] - : T extends `${infer P1} ${infer P2} <${infer P3}> <${infer P4}> [...${infer P5}]` - ? [string, string, string[]] - : T extends `${infer P1} ${infer P2} <${infer P3}> <${infer P4}> [${infer P5}]` - ? [string, string, string | undefined] - : T extends `${infer P1} ${infer P2} <${infer P3}> <${infer P4}> <${infer P5}>` - ? [string, string, string] - : T extends `${infer P1} ${infer P2} ${infer P3} <${infer P4}> [...${infer P5}]` - ? [string, string[]] - : T extends `${infer P1} ${infer P2} ${infer P3} <${infer P4}> [${infer P5}]` - ? [string, string | undefined] - : T extends `${infer P1} ${infer P2} ${infer P3} <${infer P4}> <${infer P5}>` - ? [string, string] - : T extends `<${infer P1}> <${infer P2}> <${infer P3}> [...${infer P4}]` - ? [string, string, string, string[]] - : T extends `<${infer P1}> <${infer P2}> <${infer P3}> [${infer P4}]` - ? [string, string, string, string | undefined] - : T extends `<${infer P1}> <${infer P2}> <${infer P3}> <${infer P4}>` - ? [string, string, string, string] - : T extends `${infer P1} <${infer P2}> <${infer P3}> [...${infer P4}]` - ? [string, string, string[]] - : T extends `${infer P1} <${infer P2}> <${infer P3}> [${infer P4}]` - ? [string, string, string | undefined] - : T extends `${infer P1} <${infer P2}> <${infer P3}> <${infer P4}>` - ? [string, string, string] - : T extends `${infer P1} ${infer P2} <${infer P3}> [...${infer P4}]` - ? [string, string[]] - : T extends `${infer P1} ${infer P2} <${infer P3}> [${infer P4}]` - ? [string, string | undefined] - : T extends `${infer P1} ${infer P2} <${infer P3}> <${infer P4}>` - ? [string, string] - : T extends `${infer P1} ${infer P2} ${infer P3} [...${infer P4}]` - ? [string[]] - : T extends `${infer P1} ${infer P2} ${infer P3} [${infer P4}]` - ? [string | undefined] - : T extends `${infer P1} ${infer P2} ${infer P3} <${infer P4}>` - ? [string] - : T extends `<${infer P1}> <${infer P2}> [...${infer P3}]` - ? [string, string, string[]] - : T extends `<${infer P1}> <${infer P2}> [${infer P3}]` - ? [string, string, string | undefined] - : T extends `<${infer P1}> <${infer P2}> <${infer P3}>` - ? [string, string, string] - : T extends `${infer P1} <${infer P2}> [...${infer P3}]` - ? [string, string[]] - : T extends `${infer P1} <${infer P2}> [${infer P3}]` - ? [string, string | undefined] - : T extends `${infer P1} <${infer P2}> <${infer P3}>` - ? [string, string] - : T extends `${infer P1} ${infer P2} [...${infer P3}]` - ? [string[]] - : T extends `${infer P1} ${infer P2} [${infer P3}]` - ? [string | undefined] - : T extends `${infer P1} ${infer P2} <${infer P3}>` - ? [string] - : T extends `${infer P1} ${infer P2} ${infer P3}` - ? [] - : T extends `<${infer P1}> [...${infer P2}]` - ? [string, string[]] - : T extends `<${infer P1}> [${infer P2}]` - ? [string, string | undefined] - : T extends `<${infer P1}> <${infer P2}>` - ? [string, string] - : T extends `${infer P1} [...${infer P2}]` - ? [string[]] - : T extends `${infer P1} [${infer P2}]` - ? [string | undefined] - : T extends `${infer P1} <${infer P2}>` - ? [string] - : T extends `${infer P1} ${infer P2}` - ? [] - : T extends `[...${infer P1}]` - ? [string[]] - : T extends `[${infer P1}]` - ? [string | undefined] - : T extends `<${infer P1}>` - ? [string] - : T extends `${infer P1}` - ? [] - : T extends `` - ? [] - : never; - -export type ExtractArgument = T extends `<${infer R}>` - ? string - : T extends `[...${infer R}]` - ? string[] - : T extends `[${infer R}]` - ? string | undefined - : never; +export interface Breadc { + option( + text: string, + option?: { description?: string; default?: string } + ): Breadc; + + command(text: string, option?: { description?: string }): Command; + + parse(args: string[]): { command?: Command } & ParseResult; + + run(args: string[]): Promise; +} + +export interface Command { + callback?: ActionFn; + + description: string; + + arguments: Argument[]; + + option( + text: string, + option?: { description?: string; default?: string } + ): Command; + + action(fn: ActionFn): Breadc; +} + +export interface Option { + format: F; + name: string; + short?: string; + type: 'boolean' | 'string'; + default?: T; + description: string; +} + +export interface Argument { + type: 'const' | 'require' | 'optional' | 'rest'; + name: string; +} diff --git a/packages/breadc/test/breadc.test.ts b/packages/breadc/test/breadc.test.ts index cf4af185..c0fd5317 100644 --- a/packages/breadc/test/breadc.test.ts +++ b/packages/breadc/test/breadc.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import Breadc from '../src'; -describe('Run Breadc', () => { +describe('Breadc', () => { it('should run sub commands', async () => { const cli = Breadc('cli'); cli.command('pages build'); @@ -69,7 +69,7 @@ describe('Run Breadc', () => { cli.run(['0x11', '0x12']); }); - it('should run with boolean option', () => { + it('should run with boolean option', async () => { const cli = Breadc('cal'); cli .command('minus ') @@ -87,9 +87,9 @@ describe('Run Breadc', () => { expect(typeof b).toBe('string'); expect(option.minus).toBeTruthy(); }); - cli.run(['--minus', '1', '2']); - cli.run(['minus', '1', '2']); - cli.run(['minus', '1', '2', '--no-minus']); + await cli.run(['--minus', '1', '2']); + await cli.run(['minus', '1', '2']); + // await cli.run(['minus', '1', '2', '--no-minus']); }); it('should run with required option', async () => { @@ -100,131 +100,136 @@ describe('Run Breadc', () => { .option('--fst ') .option('--snd ') .action((option) => { - expect(option.fst).toBe('1'); - expect(option.snd).toBe('2'); + // expect(option.fst).toBe('1'); + // expect(option.snd).toBe('2'); + return [option.fst, option.snd]; }); - await cli.run(['--fst', '1', '--snd', '2']); - } - { - const cli = Breadc('cal'); - cli - .command('') - .option('--fst ') - .option('--snd ') - .action((option) => { - expect(option.fst).toBeTruthy(); - expect(option.snd).toBeTruthy(); - }); - await cli.run(['--fst', '--snd']); - } - { - const cli = Breadc('cal'); - cli - .command('') - .option('--fst ') - .option('--snd ') - .action((option) => { - expect(option.fst).toBeTruthy(); - expect(option.snd).toBeFalsy(); - }); - await cli.run(['--fst']); - } - { - const cli = Breadc('cal'); - cli - .command('') - .option('--fst ') - .option('--snd ') - .action((option) => { - expect(option.fst).toBeFalsy(); - expect(option.snd).toBeFalsy(); - }); - await cli.run(['--no-fst']); + expect(await cli.run(['--fst', '1', '--snd', '2'])).toStrictEqual([ + '1', + '2' + ]); } + // { + // // error + // const cli = Breadc('cal'); + // cli + // .command('') + // .option('--fst ') + // .option('--snd ') + // .action((option) => { + // expect(option.fst).toBeTruthy(); + // expect(option.snd).toBeTruthy(); + // }); + // await cli.run(['--fst', '--snd']); + // } + // { + // const cli = Breadc('cal'); + // cli + // .command('') + // .option('--fst ') + // .option('--snd ') + // .action((option) => { + // expect(option.fst).toBeTruthy(); + // expect(option.snd).toBeFalsy(); + // }); + // await cli.run(['--fst']); + // } + // { + // const cli = Breadc('cal'); + // cli + // .command('') + // .option('--fst ') + // .option('--snd ') + // .action((option) => { + // expect(option.fst).toBeFalsy(); + // expect(option.snd).toBeFalsy(); + // }); + // await cli.run(['--no-fst']); + // } }); - it('should run with non-required option', async () => { - { - const cli = Breadc('cal'); - cli - .command('') - .option('--fst [fst]') - .option('--snd [snd]') - .action((option) => { - expect(option.fst).toBe('1'); - expect(option.snd).toBe('2'); - }); - await cli.run(['--fst', '1', '--snd', '2']); - } - { - const cli = Breadc('cal'); - cli - .command('') - .option('--fst [fst]') - .option('--snd [snd]') - .action((option) => { - expect(option.fst).toBe(''); - expect(option.snd).toBe(''); - }); - await cli.run(['--fst', '--snd']); - } - { - const cli = Breadc('cal'); - cli - .command('') - .option('--fst [fst]') - .option('--snd [snd]') - .action((option) => { - expect(option.fst).toBe(''); - expect(option.snd).toBeUndefined(); - }); - await cli.run(['--fst']); - } - { - const cli = Breadc('cal'); - cli - .command('') - .option('--fst [fst]') - .option('--snd [snd]') - .action((option) => { - expect(option.fst).toBeUndefined(); - expect(option.snd).toBeUndefined(); - }); - await cli.run(['--no-fst']); - } - }); + // it('should run with non-required option', async () => { + // { + // const cli = Breadc('cal'); + // cli + // .command('') + // .option('--fst [fst]') + // .option('--snd [snd]') + // .action((option) => { + // expect(option.fst).toBe('1'); + // expect(option.snd).toBe('2'); + // }); + // await cli.run(['--fst', '1', '--snd', '2']); + // } + // { + // const cli = Breadc('cal'); + // cli + // .command('') + // .option('--fst [fst]') + // .option('--snd [snd]') + // .action((option) => { + // expect(option.fst).toBe(''); + // expect(option.snd).toBe(''); + // }); + // await cli.run(['--fst', '--snd']); + // } + // { + // const cli = Breadc('cal'); + // cli + // .command('') + // .option('--fst [fst]') + // .option('--snd [snd]') + // .action((option) => { + // expect(option.fst).toBe(''); + // expect(option.snd).toBeUndefined(); + // }); + // await cli.run(['--fst']); + // } + // { + // const cli = Breadc('cal'); + // cli + // .command('') + // .option('--fst [fst]') + // .option('--snd [snd]') + // .action((option) => { + // expect(option.fst).toBeUndefined(); + // expect(option.snd).toBeUndefined(); + // }); + // await cli.run(['--no-fst']); + // } + // }); - it('should run with construct option', async () => { - { - const cli = Breadc('echo', { version: '1.0.0' }) - .option('--host ', { default: 'localhost' }) - .option('--port ', { - construct: (port) => (port ? +port : 3000) - }); + // it('should run with construct option', async () => { + // { + // const cli = Breadc('echo', { version: '1.0.0' }) + // .option('--host ', { default: 'localhost' }) + // .option('--port ', { + // construct: (port) => (port ? +port : 3000) + // }); - cli.command('[message]').action((_message, option) => { - expect(option.host).toBe('localhost'); - expect(option.port).toBe(3000); - }); + // cli.command('[message]').action((_message, option) => { + // expect(option.host).toBe('localhost'); + // expect(option.port).toBe(3000); + // }); - await cli.run([]); - await cli.run(['--port', '3000']); - } - { - const cli = Breadc('echo', { version: '1.0.0' }) - .option('--host ', { default: 'localhost' }) - .option('--port ', { - construct: (port) => (port ? +port : 3000) - }); + // await cli.run([]); + // await cli.run(['--port', '3000']); + // } + // { + // const cli = Breadc('echo', { version: '1.0.0' }) + // .option('--host ', { default: 'localhost' }) + // .option('--port ', { + // construct: (port) => (port ? +port : 3000) + // }); - cli.command('[message]').action((_message, option) => { - expect(option.host).toBe('ip'); - expect(option.port).toBe(3001); - }); + // cli.command('[message]').action((_message, option) => { + // expect(option.host).toBe('ip'); + // expect(option.port).toBe(3001); + // }); - await cli.run(['--host', 'ip', '--port', '3001']); - } - }); + // await cli.run(['--host', 'ip', '--port', '3001']); + // } + // }); it('has different options', async () => { const cli = Breadc('cli'); @@ -255,89 +260,89 @@ describe('Run Breadc', () => { await cli.run(['b', '--port']); }); - it('should run with default true boolean option value', async () => { - const cli = Breadc('cli'); - cli - .option('--flag', { default: true }) - .command('') - .action((option) => option.flag); - expect(await cli.run([])).toBe(true); - expect(await cli.run(['--flag'])).toBe(true); - expect(await cli.run(['--no-flag'])).toBe(false); - }); + // it('should run with default true boolean option value', async () => { + // const cli = Breadc('cli'); + // cli + // .option('--flag', { default: true }) + // .command('') + // .action((option) => option.flag); + // expect(await cli.run([])).toBe(true); + // expect(await cli.run(['--flag'])).toBe(true); + // expect(await cli.run(['--no-flag'])).toBe(false); + // }); - it('should run with default false boolean option value', async () => { - const cli = Breadc('cli'); - cli - .option('--flag', { default: false }) - .command('') - .action((option) => option.flag); - expect(await cli.run([])).toBe(false); - expect(await cli.run(['--flag'])).toBe(true); - expect(await cli.run(['--no-flag'])).toBe(false); - }); + // it('should run with default false boolean option value', async () => { + // const cli = Breadc('cli'); + // cli + // .option('--flag', { default: false }) + // .command('') + // .action((option) => option.flag); + // expect(await cli.run([])).toBe(false); + // expect(await cli.run(['--flag'])).toBe(true); + // expect(await cli.run(['--no-flag'])).toBe(false); + // }); - it('should run with default string option value', async () => { - const cli = Breadc('cli'); - cli - .option('--flag [value]', { default: 'true' }) - .command('') - .action((option) => option.flag); - expect(await cli.run([])).toBe('true'); - expect(await cli.run(['--flag'])).toBe('true'); - // TODO: fix this behaivor - expect(await cli.run(['--no-flag'])).toBe('true'); - }); + // it('should run with default string option value', async () => { + // const cli = Breadc('cli'); + // cli + // .option('--flag [value]', { default: 'true' }) + // .command('') + // .action((option) => option.flag); + // expect(await cli.run([])).toBe('true'); + // expect(await cli.run(['--flag'])).toBe('true'); + // // TODO: fix this behaivor + // expect(await cli.run(['--no-flag'])).toBe('true'); + // }); - it('should run with default string required value', async () => { - const cli = Breadc('cli'); - cli - .option('--flag ', { default: 'true' }) - .option('--open ', { default: true }) - .command('') - .action((option) => option.flag); - expect(await cli.run([])).toBe('true'); - expect(await cli.run(['--flag'])).toBe(true); - expect(await cli.run(['--flag', 'text'])).toBe('text'); - // TODO: fix this behaivor - expect(await cli.run(['--no-flag'])).toBe('true'); - }); + // it('should run with default string required value', async () => { + // const cli = Breadc('cli'); + // cli + // .option('--flag ', { default: 'true' }) + // .option('--open ', { default: true }) + // .command('') + // .action((option) => option.flag); + // expect(await cli.run([])).toBe('true'); + // expect(await cli.run(['--flag'])).toBe(true); + // expect(await cli.run(['--flag', 'text'])).toBe('text'); + // // TODO: fix this behaivor + // expect(await cli.run(['--no-flag'])).toBe('true'); + // }); }); -describe('Warnings', () => { - it('should find option conflict', async () => { - const output: string[] = []; - const cli = Breadc('cli', { - logger: { - warn(message: string) { - output.push(message); - } - } - }).option('--host [string]'); - cli.command('').option('--host'); +// describe('Warnings', () => { +// it('should find option conflict', async () => { +// const output: string[] = []; +// const cli = Breadc('cli', { +// logger: { +// warn(message: string) { +// output.push(message); +// } +// } +// }).option('--host [string]'); +// cli.command('').option('--host'); - await cli.run([]); +// await cli.run([]); - expect(output[0]).toMatchInlineSnapshot( - '"Option \\"host\\" encounters conflict"' - ); - }); +// expect(output[0]).toMatchInlineSnapshot( +// '"Option \\"host\\" encounters conflict"' +// ); +// }); - it('should not find option conflict', async () => { - const output: string[] = []; - const cli = Breadc('cli', { - logger: { - warn(message: string) { - output.push(message); - } - } - }).option('--host'); - cli.command('').option('--host'); +// it('should not find option conflict', async () => { +// const output: string[] = []; +// const cli = Breadc('cli', { +// logger: { +// warn(message: string) { +// output.push(message); +// } +// } +// }).option('--host'); +// cli.command('').option('--host'); - await cli.run([]); +// await cli.run([]); - expect(output[0]).toMatchInlineSnapshot( - '"You may miss action function in "' - ); - }); -}); +// expect(output[0]).toMatchInlineSnapshot( +// '"You may miss action function in "' +// ); +// }); +// }); diff --git a/packages/breadc/test/command.test.ts b/packages/breadc/test/command.test.ts index 25ce7c54..b164d65c 100644 --- a/packages/breadc/test/command.test.ts +++ b/packages/breadc/test/command.test.ts @@ -1,196 +1,202 @@ import { describe, expect, it } from 'vitest'; -import Breadc from '../src'; - -describe('Alias command', () => { - it('share alias', async () => { - const cli = Breadc('cli'); - let cnt = 0; - cli - .command('echo') - .alias('say') - .action(() => { - cnt++; - }); - await cli.run(['echo']); - await cli.run(['say']); - expect(cnt).toBe(2); - }); - - it('share alias with default command', async () => { - const cli = Breadc('cli'); - let cnt = 0; - cli - .command('') - .alias('echo') - .action(() => { - cnt++; - }); - await cli.run(['']); - await cli.run(['echo']); - expect(cnt).toBe(2); - }); - - it('share alias with default command and arguments', async () => { - const cli = Breadc('cli'); - let text = ''; - cli - .command('[message]') - .alias('echo') - .action((message) => { - text += message; - }); - await cli.run(['hello']); - await cli.run(['echo', ' world']); - expect(text).toBe('hello world'); - }); -}); +import breadc from '../src'; -describe('Version command', () => { - it('should print version', async () => { - const output: string[] = []; - const cli = Breadc('cli', { - version: '1.0.0', - logger: (message: string) => { - output.push(message); - } - }); - - await cli.run(['-v']); - await cli.run(['--version']); - - expect(output[0]).toMatchInlineSnapshot('"cli/1.0.0"'); - expect(output[1]).toMatchInlineSnapshot('"cli/1.0.0"'); +describe('Version Command', () => { + it('should print version', () => { + const cli = breadc('cli', { version: '0.0.0' }); }); }); -describe('Help command', () => { - it('should print simple help', async () => { - const output: string[] = []; - - const cli = Breadc('cli', { - version: '1.0.0', - description: 'This is a cli app.', - logger: (message: string) => { - output.push(message); - } - }); - cli.command('[root]', 'Start dev server'); - cli.command('build [root]', 'Build static site'); - - await cli.run(['-h']); - expect(output.join('\n')).toMatchInlineSnapshot(` - "cli/1.0.0 - - This is a cli app. - - Usage: - $ cli [root] - - Commands: - $ cli [root] Start dev server - $ cli build [root] Build static site - - Options: - -h, --help Display this message - -v, --version Display version number - " - `); - output.splice(0); - - await cli.run(['--help']); - expect(output.join('\n')).toMatchInlineSnapshot(` - "cli/1.0.0 - - This is a cli app. - - Usage: - $ cli [root] - - Commands: - $ cli [root] Start dev server - $ cli build [root] Build static site - - Options: - -h, --help Display this message - -v, --version Display version number - " - `); - }); - - it('should print command help', async () => { - const output: string[] = []; - - const cli = Breadc('cli', { - version: '1.0.0', - description: 'This is a cli app.', - logger: (message: string) => { - output.push(message); - } - }); - cli.command('[root]', 'Start dev server'); - cli.command('build [root]', 'Build static site'); - - await cli.run(['build', '--help']); - expect(output.join('\n')).toMatchInlineSnapshot(` - "cli/1.0.0 - - Build static site - - Usage: - $ cli build [root] - - Options: - -h, --help Display this message - -v, --version Display version number - " - `); - }); - - it('should print subcommands help', async () => { - const output: string[] = []; - - const cli = Breadc('cli', { - version: '1.0.0', - description: 'This is a cli app.', - logger: (message: string) => { - output.push(message); - } - }); - cli.command('file info [path]', 'Get file info'); - cli.command('store ls [path]', 'List path'); - cli.command('store rm [path]', 'Remove path'); - cli.command('store put [path]', 'Put path'); - - await cli.run(['store', '-h']); - expect(output.join('\n')).toMatchInlineSnapshot(` - "cli/1.0.0 - - Commands: - $ cli store ls [path] List path - $ cli store rm [path] Remove path - $ cli store put [path] Put path - - Options: - -h, --help Display this message - -v, --version Display version number - " - `); - - output.splice(0); - await cli.run(['--help', 'store', 'ls']); - expect(output.join('\n')).toMatchInlineSnapshot(` - "cli/1.0.0 - - List path - - Usage: - $ cli store ls [path] - - Options: - -h, --help Display this message - -v, --version Display version number - " - `); - }); -}); +// describe('Alias command', () => { +// it('share alias', async () => { +// const cli = Breadc('cli'); +// let cnt = 0; +// cli +// .command('echo') +// .alias('say') +// .action(() => { +// cnt++; +// }); +// await cli.run(['echo']); +// await cli.run(['say']); +// expect(cnt).toBe(2); +// }); + +// it('share alias with default command', async () => { +// const cli = Breadc('cli'); +// let cnt = 0; +// cli +// .command('') +// .alias('echo') +// .action(() => { +// cnt++; +// }); +// await cli.run(['']); +// await cli.run(['echo']); +// expect(cnt).toBe(2); +// }); + +// it('share alias with default command and arguments', async () => { +// const cli = Breadc('cli'); +// let text = ''; +// cli +// .command('[message]') +// .alias('echo') +// .action((message) => { +// text += message; +// }); +// await cli.run(['hello']); +// await cli.run(['echo', ' world']); +// expect(text).toBe('hello world'); +// }); +// }); + +// describe('Version command', () => { +// it('should print version', async () => { +// const output: string[] = []; +// const cli = Breadc('cli', { +// version: '1.0.0', +// logger: (message: string) => { +// output.push(message); +// } +// }); + +// await cli.run(['-v']); +// await cli.run(['--version']); + +// expect(output[0]).toMatchInlineSnapshot('"cli/1.0.0"'); +// expect(output[1]).toMatchInlineSnapshot('"cli/1.0.0"'); +// }); +// }); + +// describe('Help command', () => { +// it('should print simple help', async () => { +// const output: string[] = []; + +// const cli = Breadc('cli', { +// version: '1.0.0', +// description: 'This is a cli app.', +// logger: (message: string) => { +// output.push(message); +// } +// }); +// cli.command('[root]', 'Start dev server'); +// cli.command('build [root]', 'Build static site'); + +// await cli.run(['-h']); +// expect(output.join('\n')).toMatchInlineSnapshot(` +// "cli/1.0.0 + +// This is a cli app. + +// Usage: +// $ cli [root] + +// Commands: +// $ cli [root] Start dev server +// $ cli build [root] Build static site + +// Options: +// -h, --help Display this message +// -v, --version Display version number +// " +// `); +// output.splice(0); + +// await cli.run(['--help']); +// expect(output.join('\n')).toMatchInlineSnapshot(` +// "cli/1.0.0 + +// This is a cli app. + +// Usage: +// $ cli [root] + +// Commands: +// $ cli [root] Start dev server +// $ cli build [root] Build static site + +// Options: +// -h, --help Display this message +// -v, --version Display version number +// " +// `); +// }); + +// it('should print command help', async () => { +// const output: string[] = []; + +// const cli = Breadc('cli', { +// version: '1.0.0', +// description: 'This is a cli app.', +// logger: (message: string) => { +// output.push(message); +// } +// }); +// cli.command('[root]', 'Start dev server'); +// cli.command('build [root]', 'Build static site'); + +// await cli.run(['build', '--help']); +// expect(output.join('\n')).toMatchInlineSnapshot(` +// "cli/1.0.0 + +// Build static site + +// Usage: +// $ cli build [root] + +// Options: +// -h, --help Display this message +// -v, --version Display version number +// " +// `); +// }); + +// it('should print subcommands help', async () => { +// const output: string[] = []; + +// const cli = Breadc('cli', { +// version: '1.0.0', +// description: 'This is a cli app.', +// logger: (message: string) => { +// output.push(message); +// } +// }); +// cli.command('file info [path]', 'Get file info'); +// cli.command('store ls [path]', 'List path'); +// cli.command('store rm [path]', 'Remove path'); +// cli.command('store put [path]', 'Put path'); + +// await cli.run(['store', '-h']); +// expect(output.join('\n')).toMatchInlineSnapshot(` +// "cli/1.0.0 + +// Commands: +// $ cli store ls [path] List path +// $ cli store rm [path] Remove path +// $ cli store put [path] Put path + +// Options: +// -h, --help Display this message +// -v, --version Display version number +// " +// `); + +// output.splice(0); +// await cli.run(['--help', 'store', 'ls']); +// expect(output.join('\n')).toMatchInlineSnapshot(` +// "cli/1.0.0 + +// List path + +// Usage: +// $ cli store ls [path] + +// Options: +// -h, --help Display this message +// -v, --version Display version number +// " +// `); +// }); +// }); diff --git a/packages/breadc/test/index.test.ts b/packages/breadc/test/index.test.ts deleted file mode 100644 index 12183408..00000000 --- a/packages/breadc/test/index.test.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import Breadc from '../src'; - -describe('Parse', () => { - const logger = () => {}; - - it('should parse', () => { - expect(Breadc('cli', { logger }).parse(['hello', 'world'])) - .toMatchInlineSnapshot(` - { - "--": [], - "arguments": [ - "hello", - "world", - ], - "command": undefined, - "options": {}, - } - `); - }); - - it('should parse boolean option', () => { - expect(Breadc('cli', { logger }).parse(['--root'])).toMatchInlineSnapshot(` - { - "--": [], - "arguments": [], - "command": undefined, - "options": {}, - } - `); - - expect(Breadc('cli', { logger }).parse(['--root', 'folder'])) - .toMatchInlineSnapshot(` - { - "--": [], - "arguments": [], - "command": undefined, - "options": {}, - } - `); - - expect(Breadc('cli', { logger }).parse(['--root', 'folder', 'text'])) - .toMatchInlineSnapshot(` - { - "--": [], - "arguments": [ - "text", - ], - "command": undefined, - "options": {}, - } - `); - - expect(Breadc('cli').option('--root').parse(['--root', 'folder', 'text'])) - .toMatchInlineSnapshot(` - { - "--": [], - "arguments": [ - "folder", - "text", - ], - "command": undefined, - "options": { - "root": true, - }, - } - `); - - expect(Breadc('cli').option('--root').parse(['folder', '--root', 'text'])) - .toMatchInlineSnapshot(` - { - "--": [], - "arguments": [ - "folder", - "text", - ], - "command": undefined, - "options": { - "root": true, - }, - } - `); - - expect(Breadc('cli').option('--root').parse(['folder', 'text', '--root'])) - .toMatchInlineSnapshot(` - { - "--": [], - "arguments": [ - "folder", - "text", - ], - "command": undefined, - "options": { - "root": true, - }, - } - `); - }); - - it('should parse boolean option with shortcut', () => { - const parser = Breadc('cli').option('-r, --root'); - - expect(parser.parse([])).toMatchInlineSnapshot(` - { - "--": [], - "arguments": [], - "command": undefined, - "options": { - "root": false, - }, - } - `); - - expect(parser.parse(['--root'])).toMatchInlineSnapshot(` - { - "--": [], - "arguments": [], - "command": undefined, - "options": { - "root": true, - }, - } - `); - - expect(parser.parse(['-r'])).toMatchInlineSnapshot(` - { - "--": [], - "arguments": [], - "command": undefined, - "options": { - "root": true, - }, - } - `); - - expect(parser.parse(['-r', 'root'])).toMatchInlineSnapshot(` - { - "--": [], - "arguments": [ - "root", - ], - "command": undefined, - "options": { - "root": true, - }, - } - `); - - expect(parser.parse(['root', '-r'])).toMatchInlineSnapshot(` - { - "--": [], - "arguments": [ - "root", - ], - "command": undefined, - "options": { - "root": true, - }, - } - `); - }); - - it('should not parse wrong option', () => { - const output: string[] = []; - Breadc('cli', { - logger: { - println(message: string) { - output.push(message); - }, - warn(message: string) { - output.push(message); - } - } - }).option('invalid'); - - expect(output[0]).toMatchInlineSnapshot( - '"Can not parse option format from \\"invalid\\""' - ); - }); - - it('should receive rest arguments', async () => { - const cli = Breadc('cli'); - - cli.command('').action((option) => option['--']); - cli.command('echo [msg]').action((msg, option) => [msg, option['--']]); - - expect(await cli.run(['a', 'b', 'c'])).toMatchInlineSnapshot(` - [ - "a", - "b", - "c", - ] - `); - - expect(await cli.run(['echo', 'hello', 'world'])).toMatchInlineSnapshot(` - [ - "hello", - [ - "world", - ], - ] - `); - - expect(await cli.run(['echo', '--', 'hello', 'world'])) - .toMatchInlineSnapshot(` - [ - undefined, - [ - "hello", - "world", - ], - ] - `); - - expect(await cli.run(['--', 'echo', 'hello', 'world'])) - .toMatchInlineSnapshot(` - [ - "echo", - "hello", - "world", - ] - `); - }); -}); - -describe('Infer type', () => { - it('should run dev', async () => { - const cliWithOption = Breadc('cli').option('--root'); - const cmd = cliWithOption.command('dev'); - - cmd.action((option) => option); - - expect(await cliWithOption.run(['dev', '--root'])).toMatchInlineSnapshot(` - { - "--": [], - "root": true, - } - `); - }); - - it('should have no type', async () => { - const cliWithOption = Breadc('cli').option('--root'); - const cmd = cliWithOption.command('dev'); - - cmd.action((option) => option); - - expect(await cliWithOption.run(['dev', '--root'])).toMatchInlineSnapshot(` - { - "--": [], - "root": true, - } - `); - }); - - it('should have one type (string | undefined)', async () => { - const cliWithOption = Breadc('cli').option('--root'); - const cmd = cliWithOption.command('dev [root]'); - - cmd.action((root, option) => [root, option]); - - expect(await cliWithOption.run(['dev', '--root'])).toMatchInlineSnapshot(` - [ - undefined, - { - "--": [], - "root": true, - }, - ] - `); - }); - - it('should have one type (string)', async () => { - const cliWithOption = Breadc('cli').option('--root'); - const cmd = cliWithOption.command('dev '); - - cmd.action((root, option) => [root, option]); - - expect(await cliWithOption.run(['dev', '.', '--root'])) - .toMatchInlineSnapshot(` - [ - ".", - { - "--": [], - "root": true, - }, - ] - `); - }); -}); diff --git a/packages/breadc/test/lexer.test.ts b/packages/breadc/test/lexer.test.ts new file mode 100644 index 00000000..1f427095 --- /dev/null +++ b/packages/breadc/test/lexer.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest'; + +import { Lexer } from '../src/parser'; + +describe('lexer', () => { + it('should list all', () => { + const lexer = new Lexer(['a', 'b', 'c']); + expect([...lexer]).toMatchInlineSnapshot(` + [ + Token { + "text": "a", + }, + Token { + "text": "b", + }, + Token { + "text": "c", + }, + ] + `); + }); + + it('can use for and next', () => { + const lexer = new Lexer(['a', 'b', 'c']); + for (const token of lexer) { + if (token.raw() === 'a') { + expect(lexer.next()?.raw()).toBe('b'); + } else { + expect(token.raw()).toBe('c'); + expect(lexer.peek()).toBe(undefined); + } + } + }); + + it('can parse arg type', () => { + const lexer = new Lexer(['--', '-', '123', '-1', '--flag', '-f', 'abc']); + + const t1 = lexer.next()!; + expect(t1.type()).toBe('--'); + + const t2 = lexer.next()!; + expect(t2.type()).toBe('-'); + + const t3 = lexer.next()!; + expect(t3.type()).toBe('number'); + expect(t3.number()).toBe(123); + + const t4 = lexer.next()!; + expect(t4.type()).toBe('number'); + expect(t4.number()).toBe(-1); + + const t5 = lexer.next()!; + expect(t5.type()).toBe('long'); + expect(t5.option()).toBe('flag'); + + const t6 = lexer.next()!; + expect(t6.type()).toBe('short'); + expect(t6.option()).toBe('f'); + + const t7 = lexer.next()!; + expect(t7.type()).toBe('string'); + + const t8 = lexer.next(); + expect(t8).toBe(undefined); + }); +}); diff --git a/packages/breadc/test/parse.bench.ts b/packages/breadc/test/parse.bench.ts index 209cdfb3..21e37e56 100644 --- a/packages/breadc/test/parse.bench.ts +++ b/packages/breadc/test/parse.bench.ts @@ -1,13 +1,11 @@ import { bench, describe } from 'vitest'; import cac from 'cac'; -import Breadc from '../src'; - -import { breadc } from '../src/parser'; +import breadc from '../src'; describe('Init empty cli', () => { bench('Breadc', () => { - Breadc('cli', { version: '0.0.0', description: 'This is an empty cli' }); + breadc('cli', { version: '0.0.0', description: 'This is an empty cli' }); }); bench('cac', () => { @@ -15,14 +13,10 @@ describe('Init empty cli', () => { cli.help(); cli.version('0.0.0'); }); - - bench('Breadc Experimental', () => { - breadc('cli'); - }); }); describe('Parse option', () => { - const b = Breadc('cli'); + const b = breadc('cli'); b.option('--flag') .command('') .action(() => {}); @@ -32,11 +26,6 @@ describe('Parse option', () => { .command('') .action(() => {}); - const d = breadc('cli'); - d.option('--flag') - .command('') - .action(() => {}); - const args = ['--flag']; bench('Breadc', () => { @@ -46,22 +35,15 @@ describe('Parse option', () => { bench('cac', () => { c.parse(args); }); - - bench('Breadc Experimental', () => { - d.parse(args); - }); }); describe('Parse array', () => { - const b = Breadc('cli'); + const b = breadc('cli'); b.command('[...files]').action(() => {}); const c = cac('cli'); c.command('[...files]').action(() => {}); - const d = breadc('cli'); - d.command('[...files]').action(() => {}); - const args = ['a', 'b', 'c', 'd']; bench('Breadc', () => { @@ -71,8 +53,4 @@ describe('Parse array', () => { bench('cac', () => { c.parse(args); }); - - bench('Breadc Experimental', () => { - d.parse(args); - }); }); diff --git a/packages/breadc/test/parse.test.ts b/packages/breadc/test/parse.test.ts new file mode 100644 index 00000000..f6a78165 --- /dev/null +++ b/packages/breadc/test/parse.test.ts @@ -0,0 +1,420 @@ +import { describe, it, expect } from 'vitest'; + +import breadc from '../src'; + +const DEFAULT_ACTION = (...args: any[]) => args; + +describe('Basic Parser', () => { + it('should parse rest arguments', () => { + expect(breadc('cli').parse(['hello', 'world'])).toMatchInlineSnapshot(` + { + "--": [ + "hello", + "world", + ], + "arguments": [], + "command": undefined, + "options": {}, + } + `); + }); + + it('should receive rest arguments', async () => { + const cli = breadc('cli'); + + cli.command('').action((option) => option['--']); + cli.command('echo [msg]').action((msg, option) => [msg, option['--']]); + + expect(await cli.run(['a', 'b', 'c'])).toMatchInlineSnapshot(` + [ + "a", + "b", + "c", + ] + `); + + expect(await cli.run(['echo', 'hello', 'world'])).toMatchInlineSnapshot(` + [ + "hello", + [ + "world", + ], + ] + `); + + expect(await cli.run(['echo', '--', 'hello', 'world'])) + .toMatchInlineSnapshot(` + [ + undefined, + [ + "hello", + "world", + ], + ] + `); + + expect(await cli.run(['--', 'echo', 'hello', 'world'])) + .toMatchInlineSnapshot(` + [ + "echo", + "hello", + "world", + ] + `); + }); + + // it('should parse boolean option', () => { + // expect(breadc('cli').parse(['--root'])).toMatchInlineSnapshot(` + // { + // "--": [], + // "arguments": [], + // "command": undefined, + // "options": {}, + // } + // `); + // expect(breadc('cli').parse(['--root', 'folder'])).toMatchInlineSnapshot(` + // { + // "--": [], + // "arguments": [], + // "command": undefined, + // "options": {}, + // } + // `); + // expect(breadc('cli').parse(['--root', 'folder', 'text'])) + // .toMatchInlineSnapshot(` + // { + // "--": [], + // "arguments": [ + // "text", + // ], + // "command": undefined, + // "options": {}, + // } + // `); + // expect(breadc('cli').option('--root').parse(['--root', 'folder', 'text'])) + // .toMatchInlineSnapshot(` + // { + // "--": [], + // "arguments": [ + // "folder", + // "text", + // ], + // "command": undefined, + // "options": { + // "root": true, + // }, + // } + // `); + // expect(breadc('cli').option('--root').parse(['folder', '--root', 'text'])) + // .toMatchInlineSnapshot(` + // { + // "--": [], + // "arguments": [ + // "folder", + // "text", + // ], + // "command": undefined, + // "options": { + // "root": true, + // }, + // } + // `); + // expect(breadc('cli').option('--root').parse(['folder', 'text', '--root'])) + // .toMatchInlineSnapshot(` + // { + // "--": [], + // "arguments": [ + // "folder", + // "text", + // ], + // "command": undefined, + // "options": { + // "root": true, + // }, + // } + // `); + // }); + // it('should not parse wrong option', () => { + // Breadc('cli').option('invalid'); + // expect(output[0]).toMatchInlineSnapshot( + // '"Can not parse option format from \\"invalid\\""' + // ); + // }); +}); + +describe('Command Parser', () => { + it('should add simple commands', async () => { + const cli = breadc('cli'); + cli.command('ping').action(DEFAULT_ACTION); + cli.command('hello ').action(DEFAULT_ACTION); + cli.command('test [case]').action(DEFAULT_ACTION); + cli.command('run [...cmd]').action(DEFAULT_ACTION); + + expect(await cli.run(['ping'])).toMatchInlineSnapshot(` + [ + { + "--": [], + }, + ] + `); + expect(await cli.run(['hello', 'XLor'])).toMatchInlineSnapshot(` + [ + "XLor", + { + "--": [], + }, + ] + `); + expect(await cli.run(['test'])).toMatchInlineSnapshot(` + [ + undefined, + { + "--": [], + }, + ] + `); + expect(await cli.run(['test', 'aplusb'])).toMatchInlineSnapshot(` + [ + "aplusb", + { + "--": [], + }, + ] + `); + expect(await cli.run(['run', 'echo', '123'])).toMatchInlineSnapshot(` + [ + [ + "echo", + "123", + ], + { + "--": [], + }, + ] + `); + }); + + it('should add sub-commands', async () => { + const cli = breadc('cli'); + cli.command('dev').action(() => false); + cli.command('dev host').action(() => true); + cli.command('dev remote ').action((addr) => addr); + cli.command('dev test [root]').action((addr) => addr); + + expect(await cli.run(['dev'])).toBeFalsy(); + expect(await cli.run(['dev', 'host'])).toBeTruthy(); + expect(await cli.run(['dev', 'remote', '1.1.1.1'])).toBe('1.1.1.1'); + expect(await cli.run(['dev', 'test'])).toBe(undefined); + expect(await cli.run(['dev', 'test', '2.2.2.2'])).toBe('2.2.2.2'); + }); + + it('should add order sub-commands', async () => { + const cli = breadc('cli'); + cli.command('dev host').action(() => true); + cli.command('dev remote ').action((addr) => addr); + cli.command('dev').action(() => false); + cli.command('dev test [root]').action((addr) => addr); + + expect(await cli.run(['dev'])).toBeFalsy(); + expect(await cli.run(['dev', 'host'])).toBeTruthy(); + expect(await cli.run(['dev', 'remote', '1.1.1.1'])).toBe('1.1.1.1'); + expect(await cli.run(['dev', 'test'])).toBe(undefined); + expect(await cli.run(['dev', 'test', '2.2.2.2'])).toBe('2.2.2.2'); + }); + + it('should add default command', async () => { + const cli = breadc('cli'); + cli.command('').action((message) => message); + cli.command('dev ').action((message) => message); + cli.command('dev remote ').action((addr) => addr); + expect(await cli.run(['world'])).toBe('world'); + expect(await cli.run(['build'])).toBe('build'); + expect(await cli.run(['dev', 'world2'])).toBe('world2'); + expect(await cli.run(['dev', 'remote', '1.1.1.1'])).toBe('1.1.1.1'); + }); + + it('should add default command with optional args', async () => { + const cli = breadc('cli'); + cli.command('[message]').action((message) => message); + expect(await cli.run([])).toBe(undefined); + expect(await cli.run(['world'])).toBe('world'); + }); + + it('should add default command with rest args', async () => { + const cli = breadc('cli'); + cli.command('[...message]').action((message) => message); + expect(await cli.run([])).toStrictEqual([]); + expect(await cli.run(['world'])).toStrictEqual(['world']); + expect(await cli.run(['hello', 'world'])).toStrictEqual(['hello', 'world']); + }); +}); + +describe('Option Parser', () => { + it('should parse boolean option with shortcut', () => { + const cli = breadc('cli').option('-r, --root'); + + expect(cli.parse([])).toMatchInlineSnapshot(` + { + "--": [], + "arguments": [], + "command": undefined, + "options": { + "root": false, + }, + } + `); + + expect(cli.parse(['--root'])).toMatchInlineSnapshot(` + { + "--": [], + "arguments": [], + "command": undefined, + "options": { + "root": true, + }, + } + `); + + expect(cli.parse(['-r'])).toMatchInlineSnapshot(` + { + "--": [], + "arguments": [], + "command": undefined, + "options": { + "root": true, + }, + } + `); + + expect(cli.parse(['-r', 'root'])).toMatchInlineSnapshot(` + { + "--": [ + "root", + ], + "arguments": [], + "command": undefined, + "options": { + "root": true, + }, + } + `); + + expect(cli.parse(['root', '-r'])).toMatchInlineSnapshot(` + { + "--": [ + "root", + ], + "arguments": [], + "command": undefined, + "options": { + "root": true, + }, + } + `); + }); + + it('should parse options', async () => { + const cli = breadc('cli'); + cli.option('--remote'); + cli.option('--host '); + cli.command('').option('--flag').action(DEFAULT_ACTION); + + expect(await cli.run(['--remote'])).toMatchInlineSnapshot(` + [ + { + "--": [], + "flag": false, + "host": "", + "remote": true, + }, + ] + `); + + expect(await cli.run(['--flag'])).toMatchInlineSnapshot(` + [ + { + "--": [], + "flag": true, + "host": "", + "remote": false, + }, + ] + `); + + expect(await cli.run(['--host', '1.1.1.1'])).toMatchInlineSnapshot(` + [ + { + "--": [], + "flag": false, + "host": "1.1.1.1", + "remote": false, + }, + ] + `); + }); +}); + +describe('Infer type', () => { + it('should run dev', async () => { + const cliWithOption = breadc('cli').option('--root'); + const cmd = cliWithOption.command('dev'); + + cmd.action((option) => option); + + expect(await cliWithOption.run(['dev', '--root'])).toMatchInlineSnapshot(` + { + "--": [], + "root": true, + } + `); + }); + + it('should have no type', async () => { + const cliWithOption = breadc('cli').option('--root'); + const cmd = cliWithOption.command('dev'); + + cmd.action((option) => option); + + expect(await cliWithOption.run(['dev', '--root'])).toMatchInlineSnapshot(` + { + "--": [], + "root": true, + } + `); + }); + + it('should have one type (string | undefined)', async () => { + const cliWithOption = breadc('cli').option('--root'); + const cmd = cliWithOption.command('dev [root]'); + + cmd.action((root, option) => [root, option]); + + expect(await cliWithOption.run(['dev', '--root'])).toMatchInlineSnapshot(` + [ + undefined, + { + "--": [], + "root": true, + }, + ] + `); + }); + + it('should have one type (string)', async () => { + const cliWithOption = breadc('cli').option('--root'); + const cmd = cliWithOption.command('dev '); + + cmd.action((root, option) => [root, option]); + + expect(await cliWithOption.run(['dev', '.', '--root'])) + .toMatchInlineSnapshot(` + [ + ".", + { + "--": [], + "root": true, + }, + ] + `); + }); +}); diff --git a/packages/breadc/test/parser.test.ts b/packages/breadc/test/parser.test.ts deleted file mode 100644 index 6304d8e8..00000000 --- a/packages/breadc/test/parser.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { Lexer, breadc } from '../src/parser'; - -describe('lexer', () => { - it('should list all', () => { - const lexer = new Lexer(['a', 'b', 'c']); - expect([...lexer]).toMatchInlineSnapshot(` - [ - Token { - "text": "a", - }, - Token { - "text": "b", - }, - Token { - "text": "c", - }, - ] - `); - }); - - it('can use for and next', () => { - const lexer = new Lexer(['a', 'b', 'c']); - for (const token of lexer) { - if (token.raw() === 'a') { - expect(lexer.next()?.raw()).toBe('b'); - } else { - expect(token.raw()).toBe('c'); - expect(lexer.peek()).toBe(undefined); - } - } - }); - - it('can parse arg type', () => { - const lexer = new Lexer(['--', '-', '123', '-1', '--flag', '-f', 'abc']); - - const t1 = lexer.next()!; - expect(t1.type()).toBe('--'); - - const t2 = lexer.next()!; - expect(t2.type()).toBe('-'); - - const t3 = lexer.next()!; - expect(t3.type()).toBe('number'); - expect(t3.number()).toBe(123); - - const t4 = lexer.next()!; - expect(t4.type()).toBe('number'); - expect(t4.number()).toBe(-1); - - const t5 = lexer.next()!; - expect(t5.type()).toBe('long'); - expect(t5.option()).toBe('flag'); - - const t6 = lexer.next()!; - expect(t6.type()).toBe('short'); - expect(t6.option()).toBe('f'); - - const t7 = lexer.next()!; - expect(t7.type()).toBe('string'); - - const t8 = lexer.next(); - expect(t8).toBe(undefined); - }); -}); - -describe('parser', () => { - const DEFAULT_ACTION = (...args: any[]) => args; - - it('should add simple commands', async () => { - const cli = breadc('cli'); - cli.command('ping').action(DEFAULT_ACTION); - cli.command('hello ').action(DEFAULT_ACTION); - cli.command('test [case]').action(DEFAULT_ACTION); - cli.command('run [...cmd]').action(DEFAULT_ACTION); - - expect(await cli.run(['ping'])).toMatchInlineSnapshot(` - [ - { - "--": [], - }, - ] - `); - expect(await cli.run(['hello', 'XLor'])).toMatchInlineSnapshot(` - [ - "XLor", - { - "--": [], - }, - ] - `); - expect(await cli.run(['test'])).toMatchInlineSnapshot(` - [ - undefined, - { - "--": [], - }, - ] - `); - expect(await cli.run(['test', 'aplusb'])).toMatchInlineSnapshot(` - [ - "aplusb", - { - "--": [], - }, - ] - `); - expect(await cli.run(['run', 'echo', '123'])).toMatchInlineSnapshot(` - [ - [ - "echo", - "123", - ], - { - "--": [], - }, - ] - `); - }); - - it('should add sub-commands', async () => { - const cli = breadc('cli'); - cli.command('dev').action(() => false); - cli.command('dev host').action(() => true); - cli.command('dev remote ').action((addr) => addr); - cli.command('dev test [root]').action((addr) => addr); - - expect(await cli.run(['dev'])).toBeFalsy(); - expect(await cli.run(['dev', 'host'])).toBeTruthy(); - expect(await cli.run(['dev', 'remote', '1.1.1.1'])).toBe('1.1.1.1'); - expect(await cli.run(['dev', 'test'])).toBe(undefined); - expect(await cli.run(['dev', 'test', '2.2.2.2'])).toBe('2.2.2.2'); - }); - - it('should add order sub-commands', async () => { - const cli = breadc('cli'); - cli.command('dev host').action(() => true); - cli.command('dev remote ').action((addr) => addr); - cli.command('dev').action(() => false); - cli.command('dev test [root]').action((addr) => addr); - - expect(await cli.run(['dev'])).toBeFalsy(); - expect(await cli.run(['dev', 'host'])).toBeTruthy(); - expect(await cli.run(['dev', 'remote', '1.1.1.1'])).toBe('1.1.1.1'); - expect(await cli.run(['dev', 'test'])).toBe(undefined); - expect(await cli.run(['dev', 'test', '2.2.2.2'])).toBe('2.2.2.2'); - }); - - it('should add default command', async () => { - const cli = breadc('cli'); - cli.command('').action((message) => message); - cli.command('dev ').action((message) => message); - cli.command('dev remote ').action((addr) => addr); - expect(await cli.run(['world'])).toBe('world'); - expect(await cli.run(['build'])).toBe('build'); - expect(await cli.run(['dev', 'world2'])).toBe('world2'); - expect(await cli.run(['dev', 'remote', '1.1.1.1'])).toBe('1.1.1.1'); - }); - - it('should add default command with optional args', async () => { - const cli = breadc('cli'); - cli.command('[message]').action((message) => message); - expect(await cli.run([])).toBe(undefined); - expect(await cli.run(['world'])).toBe('world'); - }); - - it('should add default command with rest args', async () => { - const cli = breadc('cli'); - cli.command('[...message]').action((message) => message); - expect(await cli.run([])).toStrictEqual([]); - expect(await cli.run(['world'])).toStrictEqual(['world']); - expect(await cli.run(['hello', 'world'])).toStrictEqual(['hello', 'world']); - }); - - it('should parse options', async () => { - const cli = breadc('cli'); - cli.option('--remote'); - cli.option('--host '); - cli.command('').option('--flag').action(DEFAULT_ACTION); - // cli.option('--files [...host]'); - - expect(await cli.run(['--remote'])).toMatchInlineSnapshot(` - [ - { - "--": [], - "flag": false, - "host": "", - "remote": true, - }, - ] - `); - - expect(await cli.run(['--flag'])).toMatchInlineSnapshot(` - [ - { - "--": [], - "flag": true, - "host": "", - "remote": false, - }, - ] - `); - - expect(await cli.run(['--host', '1.1.1.1'])).toMatchInlineSnapshot(` - [ - { - "--": [], - "flag": false, - "host": "1.1.1.1", - "remote": false, - }, - ] - `); - }); -});