mirror of
https://github.com/chenasraf/massarg.git
synced 2026-05-18 01:39:05 +00:00
fix: flag negation
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
OPT_SHORT_PREFIX,
|
||||
NEGATE_SHORT_PREFIX,
|
||||
Prefixes,
|
||||
FlagConfig,
|
||||
} from './option'
|
||||
import { DeepRequired, setOrPush, deepMerge } from './utils'
|
||||
import { MassargExample, ExampleConfig } from './example'
|
||||
@@ -99,6 +100,10 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
|
||||
get optionPrefixes(): Prefixes {
|
||||
return this.getPrefixes()
|
||||
}
|
||||
|
||||
private getPrefixes(): Prefixes {
|
||||
return {
|
||||
optionPrefix: this.optionPrefix,
|
||||
aliasPrefix: this.optionAliasPrefix,
|
||||
@@ -177,11 +182,9 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
* or by prefixing the alias with `^` instead of `-`. This is configurable via the command's
|
||||
* configuration.
|
||||
*/
|
||||
flag(config: Omit<OptionConfig<boolean>, 'parse' | 'isDefault'>): MassargCommand<Args>
|
||||
flag(config: FlagConfig): MassargCommand<Args>
|
||||
flag(config: MassargFlag): MassargCommand<Args>
|
||||
flag(
|
||||
config: Omit<OptionConfig<boolean>, 'parse' | 'isDefault'> | MassargFlag,
|
||||
): MassargCommand<Args> {
|
||||
flag(config: FlagConfig | MassargFlag): MassargCommand<Args> {
|
||||
try {
|
||||
const flag = config instanceof MassargFlag ? config : new MassargFlag(config)
|
||||
const existing = this.options.find((c) => c.name === flag.name)
|
||||
@@ -320,15 +323,17 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
|
||||
private parseOption(arg: string, argv: string[]) {
|
||||
const option = this.options.find((o) => o._match(arg, this.optionPrefixes))
|
||||
const prefixes = { ...this.optionPrefixes }
|
||||
const option = this.options.find((o) => o._match(arg, prefixes))
|
||||
if (!option) {
|
||||
throw new ValidationError({
|
||||
path: [MassargOption.findNameInArg(arg, this.optionPrefixes)],
|
||||
path: [MassargOption.findNameInArg(arg, prefixes)],
|
||||
code: 'unknown_option',
|
||||
message: 'Unknown option',
|
||||
})
|
||||
}
|
||||
const res = option._parseDetails([arg, ...argv], { ...this.args }, this.optionPrefixes)
|
||||
const res = option.parseDetails([arg, ...argv], { ...this.args }, prefixes)
|
||||
|
||||
this.args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
|
||||
res.value,
|
||||
this.args[res.key as keyof Args],
|
||||
@@ -377,7 +382,7 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
while (_argv.length) {
|
||||
const arg = _argv.shift()!
|
||||
// make sure option exists
|
||||
const found = this.options.some((o) => o._isOption(arg, this.optionPrefixes))
|
||||
const found = this.options.some((o) => o._isOption(arg, { ...this.optionPrefixes }))
|
||||
if (found) {
|
||||
_argv = this.parseOption(arg, _argv)
|
||||
_args = { ..._args, ...this.args }
|
||||
|
||||
88
src/help.ts
88
src/help.ts
@@ -214,9 +214,9 @@ export class HelpGenerator {
|
||||
}
|
||||
const maxNameLength = this.config.useGlobalTableColumns
|
||||
? Math.max(
|
||||
getMaxNameLength(entry.options.map((e) => getItemDetails(e, optionOptions))),
|
||||
getMaxNameLength(entry.commands.map((e) => getItemDetails(e, commandOptions))),
|
||||
)
|
||||
getMaxNameLength(entry.options.map((e) => getItemDetails(e, optionOptions))),
|
||||
getMaxNameLength(entry.commands.map((e) => getItemDetails(e, commandOptions))),
|
||||
)
|
||||
: undefined
|
||||
const options = generateHelpTable(entry.options, optionOptions, maxNameLength).trimEnd()
|
||||
const commands = generateHelpTable(entry.commands, commandOptions, maxNameLength).trimEnd()
|
||||
@@ -228,21 +228,21 @@ export class HelpGenerator {
|
||||
_wrap(format(description, this.config.exampleOptions.descriptionStyle), 4),
|
||||
],
|
||||
input &&
|
||||
_wrap(
|
||||
format(
|
||||
[this.config.exampleOptions.inputPrefix, input].filter(Boolean).join(' '),
|
||||
this.config.exampleOptions.inputStyle,
|
||||
_wrap(
|
||||
format(
|
||||
[this.config.exampleOptions.inputPrefix, input].filter(Boolean).join(' '),
|
||||
this.config.exampleOptions.inputStyle,
|
||||
),
|
||||
4,
|
||||
),
|
||||
4,
|
||||
),
|
||||
output &&
|
||||
_wrap(
|
||||
format(
|
||||
[this.config.exampleOptions.outputPrefix, output].filter(Boolean).join(' '),
|
||||
this.config.exampleOptions.outputStyle,
|
||||
_wrap(
|
||||
format(
|
||||
[this.config.exampleOptions.outputPrefix, output].filter(Boolean).join(' '),
|
||||
this.config.exampleOptions.outputStyle,
|
||||
),
|
||||
4,
|
||||
),
|
||||
4,
|
||||
),
|
||||
)
|
||||
})
|
||||
.join('\n')
|
||||
@@ -253,17 +253,17 @@ export class HelpGenerator {
|
||||
_wrap(
|
||||
usageText
|
||||
? strConcat(
|
||||
format('Usage:', this.config.usageStyle.prefix),
|
||||
format(usageText, this.config.usageStyle.main),
|
||||
)
|
||||
format('Usage:', this.config.usageStyle.prefix),
|
||||
format(usageText, this.config.usageStyle.main),
|
||||
)
|
||||
: [
|
||||
format(`Usage:`, this.config.usageStyle.prefix),
|
||||
format(entry.name, this.config.usageStyle.main),
|
||||
commands.length && format('[command]', this.config.usageStyle.command),
|
||||
options.length && format('[options]', this.config.usageStyle.options),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
format(`Usage:`, this.config.usageStyle.prefix),
|
||||
format(entry.name, this.config.usageStyle.main),
|
||||
commands.length && format('[command]', this.config.usageStyle.command),
|
||||
options.length && format('[options]', this.config.usageStyle.options),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
),
|
||||
headerText.length && ['', format(headerText, this.config.descriptionStyle)],
|
||||
entry.description.length && [
|
||||
@@ -271,27 +271,27 @@ export class HelpGenerator {
|
||||
_wrap(format(entry.description, this.config.descriptionStyle)),
|
||||
],
|
||||
commands.length &&
|
||||
indent([
|
||||
'',
|
||||
format(
|
||||
entry.parent ? `Commands for ${entry.name}:` : 'Commands:',
|
||||
this.config.subtitleStyle,
|
||||
),
|
||||
'',
|
||||
indent(commands),
|
||||
]),
|
||||
indent([
|
||||
'',
|
||||
format(
|
||||
entry.parent ? `Commands for ${entry.name}:` : 'Commands:',
|
||||
this.config.subtitleStyle,
|
||||
),
|
||||
'',
|
||||
indent(commands),
|
||||
]),
|
||||
options.length &&
|
||||
indent([
|
||||
'',
|
||||
format(
|
||||
entry.parent ? `Options for ${entry.name}:` : 'Options:',
|
||||
this.config.subtitleStyle,
|
||||
),
|
||||
'',
|
||||
indent(options),
|
||||
]),
|
||||
indent([
|
||||
'',
|
||||
format(
|
||||
entry.parent ? `Options for ${entry.name}:` : 'Options:',
|
||||
this.config.subtitleStyle,
|
||||
),
|
||||
'',
|
||||
indent(options),
|
||||
]),
|
||||
examples.length &&
|
||||
indent(['', format('Examples:', this.config.subtitleStyle), '', indent(examples)]),
|
||||
indent(['', format('Examples:', this.config.subtitleStyle), '', indent(examples)]),
|
||||
footerText.length && ['', _wrap(format(footerText, this.config.descriptionStyle))],
|
||||
) + '\n'
|
||||
)
|
||||
|
||||
@@ -50,12 +50,14 @@ export type OptionConfig<T = unknown, Args extends ArgsObject = ArgsObject> = z.
|
||||
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 const FlagConfig = OptionConfig<boolean>(z.any())
|
||||
.omit({ parse: true, isDefault: true })
|
||||
.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> = (
|
||||
@@ -99,7 +101,7 @@ export type ArrayOptionConfig<T = unknown> = z.infer<
|
||||
// TODO turn to options
|
||||
export const OPT_FULL_PREFIX = '--'
|
||||
export const OPT_SHORT_PREFIX = '-'
|
||||
export const NEGATE_FULL_PREFIX = 'no-'
|
||||
export const NEGATE_FULL_PREFIX = '--no-'
|
||||
export const NEGATE_SHORT_PREFIX = '^'
|
||||
|
||||
export type Prefixes = {
|
||||
@@ -172,8 +174,7 @@ export class MassargOption<OptionType extends any = unknown, Args extends ArgsOb
|
||||
return this.outputName || toCamelCase(this.name)
|
||||
}
|
||||
|
||||
_parseDetails(argv: string[], options: ArgsObject, prefixes: Prefixes): ArgvValue<OptionType> {
|
||||
// TODO: support --option=value
|
||||
parseDetails(argv: string[], options: ArgsObject, prefixes: Prefixes): ArgvValue<OptionType> {
|
||||
let input = ''
|
||||
try {
|
||||
if (!this._match(argv[0], prefixes)) {
|
||||
@@ -221,25 +222,21 @@ export class MassargOption<OptionType extends any = unknown, Args extends ArgsOb
|
||||
|
||||
static findNameInArg(arg: string, prefixes: Prefixes): string {
|
||||
const { optionPrefix, aliasPrefix, negateFlagPrefix, negateAliasPrefix } = prefixes
|
||||
if (arg.startsWith(optionPrefix)) {
|
||||
// negate full prefix
|
||||
if (arg.startsWith(negateFlagPrefix)) {
|
||||
return arg.slice(negateFlagPrefix.length)
|
||||
}
|
||||
return arg.slice(optionPrefix.length)
|
||||
}
|
||||
// negate full prefix
|
||||
if (arg.startsWith(negateFlagPrefix)) {
|
||||
return arg.slice(negateFlagPrefix.length)
|
||||
}
|
||||
// short prefix
|
||||
if (arg.startsWith(aliasPrefix) || arg.startsWith(negateAliasPrefix)) {
|
||||
return arg.slice(aliasPrefix.length)
|
||||
if (arg.startsWith(optionPrefix)) {
|
||||
return arg.slice(optionPrefix.length)
|
||||
}
|
||||
// negate short prefix
|
||||
if (arg.startsWith(negateAliasPrefix)) {
|
||||
return arg.slice(negateAliasPrefix.length)
|
||||
}
|
||||
// short prefix
|
||||
if (arg.startsWith(aliasPrefix) || arg.startsWith(negateAliasPrefix)) {
|
||||
return arg.slice(aliasPrefix.length)
|
||||
}
|
||||
return '<blank>'
|
||||
}
|
||||
}
|
||||
@@ -268,9 +265,9 @@ export class MassargNumber extends MassargOption<number> {
|
||||
})
|
||||
}
|
||||
|
||||
_parseDetails(argv: string[], options: ArgsObject, prefixes: Prefixes): ArgvValue<number> {
|
||||
parseDetails(argv: string[], options: ArgsObject, prefixes: Prefixes): ArgvValue<number> {
|
||||
try {
|
||||
const { argv: _argv, value } = super._parseDetails(argv, options, prefixes)
|
||||
const { argv: _argv, value } = super.parseDetails(argv, options, prefixes)
|
||||
if (isNaN(value)) {
|
||||
throw new ParseError({
|
||||
path: [this.name],
|
||||
@@ -326,7 +323,7 @@ export class MassargFlag extends MassargOption<boolean> {
|
||||
this.negatable = options.negatable ?? false
|
||||
}
|
||||
|
||||
_parseDetails(argv: string[], prefixes: Prefixes): ArgvValue<boolean> {
|
||||
parseDetails(argv: string[], options: ArgsObject, prefixes: Prefixes): ArgvValue<boolean> {
|
||||
try {
|
||||
const isNegation =
|
||||
argv[0]?.startsWith(prefixes.negateAliasPrefix) ||
|
||||
@@ -350,9 +347,9 @@ export class MassargFlag extends MassargOption<boolean> {
|
||||
|
||||
argv.shift()
|
||||
if (isNegation) {
|
||||
return { key: this.name, value: false, argv }
|
||||
return { key: this.getOutputName(), value: false, argv }
|
||||
}
|
||||
return { key: this.name, value: true, argv }
|
||||
return { key: this.getOutputName(), value: true, argv }
|
||||
} catch (e) {
|
||||
if (isZodError(e)) {
|
||||
throw new ParseError({
|
||||
|
||||
20
src/utils.ts
20
src/utils.ts
@@ -91,15 +91,17 @@ export function deepMerge<T1, T2>(obj1: T1, obj2: T2): NonNullable<T1> & NonNull
|
||||
* regular spaced strings.
|
||||
*/
|
||||
export function splitWords(str: string): string[] {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace(/([a-zA-Z])([0-9])/g, '$1 $2')
|
||||
.replace(/([0-9])([a-zA-Z])/g, '$1 $2')
|
||||
.replace(/([a-z])([_-])/g, '$1 $2')
|
||||
.replace(/([_-])([a-zA-Z])/g, '$1 $2')
|
||||
.split(/[_-]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
return (
|
||||
str
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
// .replace(/([a-zA-Z])([0-9])/g, '$1 $2')
|
||||
.replace(/([0-9])([a-zA-Z])/g, '$1 $2')
|
||||
.replace(/([a-z])([_-])/g, '$1 $2')
|
||||
.replace(/([_-])([a-zA-Z])/g, '$1 $2')
|
||||
.split(/[_-]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
}
|
||||
|
||||
export function toCamelCase(str: string): string {
|
||||
|
||||
@@ -104,9 +104,20 @@ describe('flag', () => {
|
||||
).toThrow('Expected string, received number')
|
||||
})
|
||||
describe('negation', () => {
|
||||
test('default', () => {
|
||||
test('no negation', () => {
|
||||
const command = massarg(opts).flag({ name: 'test2', description: 'test2', aliases: [] })
|
||||
expect(command.getArgs(['--no-test2'])).toThrow('is not negatable')
|
||||
expect(() => command.getArgs(['--no-test2'])).toThrow(
|
||||
'test2: Option test2 cannot be negated (received: "--no-test2")',
|
||||
)
|
||||
})
|
||||
test('negation', () => {
|
||||
const command = massarg(opts).flag({
|
||||
name: 'test2',
|
||||
description: 'test2',
|
||||
aliases: [],
|
||||
negatable: true,
|
||||
})
|
||||
expect(command.getArgs(['--no-test2'])).toHaveProperty('test2', false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user