fix: detect the correct flag syntax in all cases

This commit is contained in:
2023-12-04 21:04:31 +02:00
committed by Chen Asraf
parent 0f17d336fb
commit 291ff0fe0e
3 changed files with 50 additions and 36 deletions

View File

@@ -11,6 +11,7 @@ import {
NEGATE_FULL_PREFIX, NEGATE_FULL_PREFIX,
OPT_SHORT_PREFIX, OPT_SHORT_PREFIX,
NEGATE_SHORT_PREFIX, NEGATE_SHORT_PREFIX,
Prefixes,
} from './option' } from './option'
import { DeepRequired, setOrPush, deepMerge } from './utils' import { DeepRequired, setOrPush, deepMerge } from './utils'
import { MassargExample, ExampleConfig } from './example' import { MassargExample, ExampleConfig } from './example'
@@ -97,6 +98,15 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
this.parent = parent this.parent = parent
} }
get optionPrefixes(): Prefixes {
return {
optionPrefix: this.optionPrefix,
aliasPrefix: this.optionAliasPrefix,
negateFlagPrefix: this.negateFlagPrefix,
negateAliasPrefix: this.negateAliasPrefix,
}
}
get helpConfig(): DeepRequired<HelpConfig> { get helpConfig(): DeepRequired<HelpConfig> {
if (this.parent) { if (this.parent) {
return deepMerge(this.parent.helpConfig, this._helpConfig) return deepMerge(this.parent.helpConfig, this._helpConfig)
@@ -310,10 +320,10 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
} }
private parseOption(arg: string, argv: string[]) { private parseOption(arg: string, argv: string[]) {
const option = this.options.find((o) => o._match(arg)) const option = this.options.find((o) => o._match(arg, this.optionPrefixes))
if (!option) { if (!option) {
throw new ValidationError({ throw new ValidationError({
path: [MassargOption.getName(arg)], path: [MassargOption.findNameInArg(arg, this.optionPrefixes)],
code: 'unknown_option', code: 'unknown_option',
message: 'Unknown option', message: 'Unknown option',
}) })
@@ -367,7 +377,7 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
while (_argv.length) { while (_argv.length) {
const arg = _argv.shift()! const arg = _argv.shift()!
// make sure option exists // make sure option exists
const found = this.options.some((o) => o._isOption(arg)) const found = this.options.some((o) => o._isOption(arg, this.optionPrefixes))
if (found) { if (found) {
_argv = this.parseOption(arg, _argv) _argv = this.parseOption(arg, _argv)
_args = { ..._args, ...this.args } _args = { ..._args, ...this.args }

View File

@@ -254,7 +254,7 @@ export class HelpGenerator {
usageText usageText
? strConcat( ? strConcat(
format('Usage:', this.config.usageStyle.prefix), format('Usage:', this.config.usageStyle.prefix),
format(usageText, this.config.usageStyle.command), format(usageText, this.config.usageStyle.main),
) )
: [ : [
format(`Usage:`, this.config.usageStyle.prefix), format(`Usage:`, this.config.usageStyle.prefix),

View File

@@ -50,6 +50,14 @@ export type OptionConfig<T = unknown, Args extends ArgsObject = ArgsObject> = z.
ReturnType<typeof OptionConfig<T, Args>> ReturnType<typeof OptionConfig<T, Args>>
> >
export const FlagConfig = OptionConfig<boolean>(z.any()).merge(
z.object({
/** Whether the flag can be negated, e.g. `--no-verbose` */
negatable: z.boolean().optional(),
}),
)
export type FlagConfig = z.infer<typeof FlagConfig>
export type Parser<Args extends ArgsObject = ArgsObject, OptionType extends any = any> = ( export type Parser<Args extends ArgsObject = ArgsObject, OptionType extends any = any> = (
x: string, x: string,
y: Args, y: Args,
@@ -94,6 +102,13 @@ export const OPT_SHORT_PREFIX = '-'
export const NEGATE_FULL_PREFIX = 'no-' export const NEGATE_FULL_PREFIX = 'no-'
export const NEGATE_SHORT_PREFIX = '^' export const NEGATE_SHORT_PREFIX = '^'
export type Prefixes = {
optionPrefix: string
aliasPrefix: string
negateFlagPrefix: string
negateAliasPrefix: string
}
/** @internal */ /** @internal */
export type ArgvValue<T> = { argv: string[]; value: T; key: string } export type ArgvValue<T> = { argv: string[]; value: T; key: string }
@@ -191,29 +206,11 @@ export class MassargOption<OptionType extends any = unknown, Args extends ArgsOb
return `--${this.name}${aliases} ${this.description}` return `--${this.name}${aliases} ${this.description}`
} }
_match(arg: string): boolean { _match(arg: string, prefixes: Prefixes): boolean {
if (!arg) return false return MassargOption.findNameInArg(arg, prefixes) !== '<blank>'
// full prefix
if (arg.startsWith(OPT_FULL_PREFIX)) {
// negate full prefix
if (arg.startsWith(`--${NEGATE_FULL_PREFIX}`)) {
return this.name === arg.slice(`--${NEGATE_FULL_PREFIX}`.length)
}
return this.name === arg.slice(OPT_FULL_PREFIX.length)
}
// short prefix
if (arg.startsWith(OPT_SHORT_PREFIX) || arg.startsWith(NEGATE_SHORT_PREFIX)) {
return this.aliases.includes(arg.slice(OPT_SHORT_PREFIX.length))
}
// negate short prefix
if (arg.startsWith(NEGATE_SHORT_PREFIX)) {
return this.aliases.includes(arg.slice(NEGATE_SHORT_PREFIX.length))
}
// no prefix
return false
} }
_isOption(arg: string): boolean { _isOption(arg: string, prefixes: Prefixes): boolean {
return ( return (
arg.startsWith(OPT_FULL_PREFIX) || arg.startsWith(OPT_FULL_PREFIX) ||
arg.startsWith(OPT_SHORT_PREFIX) || arg.startsWith(OPT_SHORT_PREFIX) ||
@@ -221,21 +218,25 @@ export class MassargOption<OptionType extends any = unknown, Args extends ArgsOb
) )
} }
static getName(arg: string): string { static findNameInArg(
if (arg.startsWith(OPT_FULL_PREFIX)) { arg: string,
prefixes: Prefixes,
): string {
const { optionPrefix, aliasPrefix, negateFlagPrefix, negateAliasPrefix } = prefixes
if (arg.startsWith(optionPrefix)) {
// negate full prefix // negate full prefix
if (arg.startsWith(`--${NEGATE_FULL_PREFIX}`)) { if (arg.startsWith(`--${negateFlagPrefix}`)) {
return arg.slice(`--${NEGATE_FULL_PREFIX}`.length) return arg.slice(`--${negateFlagPrefix}`.length)
} }
return arg.slice(OPT_FULL_PREFIX.length) return arg.slice(optionPrefix.length)
} }
// short prefix // short prefix
if (arg.startsWith(OPT_SHORT_PREFIX) || arg.startsWith(NEGATE_SHORT_PREFIX)) { if (arg.startsWith(aliasPrefix) || arg.startsWith(negateAliasPrefix)) {
return arg.slice(OPT_SHORT_PREFIX.length) return arg.slice(aliasPrefix.length)
} }
// negate short prefix // negate short prefix
if (arg.startsWith(NEGATE_SHORT_PREFIX)) { if (arg.startsWith(negateAliasPrefix)) {
return arg.slice(NEGATE_SHORT_PREFIX.length) return arg.slice(negateAliasPrefix.length)
} }
return '<blank>' return '<blank>'
} }
@@ -300,7 +301,7 @@ export class MassargNumber extends MassargOption<number> {
* *
* A flag can be negated by prefixing it with `no-`. For example, `--no-verbose`, * A flag can be negated by prefixing it with `no-`. For example, `--no-verbose`,
* or by prefixing the alias with `^` instead of `-`. This is configurable via the command's * or by prefixing the alias with `^` instead of `-`. This is configurable via the command's
* configuration. * configuration. To turn this behavior on, set `negatable: true` in the flag's configuration.
* *
* @example * @example
* ```ts * ```ts
@@ -313,11 +314,14 @@ export class MassargNumber extends MassargOption<number> {
* ``` * ```
*/ */
export class MassargFlag extends MassargOption<boolean> { export class MassargFlag extends MassargOption<boolean> {
constructor(options: Omit<OptionConfig<boolean>, 'parse'>) { negatable: boolean
constructor(options: FlagConfig) {
super({ super({
...options, ...options,
parse: () => true as any, parse: () => true as any,
}) })
this.negatable = options.negatable ?? false
} }
_parseDetails(argv: string[]): ArgvValue<boolean> { _parseDetails(argv: string[]): ArgvValue<boolean> {