fix: command/option parsing priorities

This commit is contained in:
2023-12-14 10:24:42 +02:00
committed by Chen Asraf
parent 64a9f7a7e5
commit 5129528339
6 changed files with 92 additions and 35 deletions

View File

@@ -14,8 +14,9 @@ import {
Prefixes,
FlagConfig,
} from './option'
import { DeepRequired, setOrPush, deepMerge } from './utils'
import { DeepRequired, setOrPush, deepMerge, getErrorMessage } from './utils'
import { MassargExample, ExampleConfig } from './example'
import { format } from './style'
export const CommandConfig = <RunArgs extends ArgsObject = ArgsObject>(args: z.ZodType<RunArgs>) =>
z.object({
@@ -187,14 +188,7 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
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)
if (existing) {
throw new ValidationError({
code: 'duplicate_flag',
message: `Flag "${flag.name}" already exists`,
path: [this.name, flag.name],
})
}
this.assertNotDuplicate(flag)
this.options.push(flag as MassargOption)
return this
} catch (e) {
@@ -234,24 +228,8 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
config instanceof MassargOption
? config
: MassargOption.fromTypedConfig(config as TypedOptionConfig<T, A>)
const existing = this.options.find((c) => c.name === option.name)
if (existing) {
throw new ValidationError({
code: 'duplicate_option',
message: `Option "${option.name}" already exists`,
path: [this.name, option.name],
})
}
if (option.isDefault) {
const defaultOption = this.options.find((o) => o.isDefault)
if (defaultOption) {
throw new ValidationError({
code: 'duplicate_default_option',
message: `Option "${option.name}" cannot be set as default because option "${defaultOption.name}" is already set as default`,
path: [this.name, option.name],
})
}
}
this.assertNotDuplicate(option)
this.assertOnlyOneDefault<T, A>(option)
this.options.push(option as MassargOption)
return this
} catch (e) {
@@ -266,6 +244,43 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
}
}
private assertNotDuplicate<T = string, A extends ArgsObject = Args>(option: MassargOption<T, A>) {
const existingName = this.options.find((c) => c.name === option.name),)
if (existingName) {
throw new ValidationError({
code: 'duplicate_option_name',
message: `Option name "${existingName.name}" already exists`,
path: [this.name, option.name],
})
}
const existingAlias = this.options.find((c) =>
c.aliases.some((a) => option.aliases.includes(a)),
)
if (existingAlias) {
const alias = option.aliases.find((a) => existingAlias.aliases.includes(a))!
throw new ValidationError({
code: 'duplicate_option_alias',
message: `Option alias "${alias}" already exists on option "${existingAlias.name}"`,
path: [this.name, option.name],
})
}
}
private assertOnlyOneDefault<T = string, A extends ArgsObject = Args>(
option: MassargOption<T, A>,
) {
if (option.isDefault) {
const defaultOption = this.options.find((o) => o.isDefault)
if (defaultOption) {
throw new ValidationError({
code: 'duplicate_default_option',
message: `Option "${option.name}" cannot be set as default because option "${defaultOption.name}" is already set as default`,
path: [this.name, option.name],
})
}
}
}
/**
* Adds an example to this command.
*
@@ -323,7 +338,12 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
args?: Partial<Args>,
parent?: MassargCommand<Args>,
): Promise<void> | void {
this.getArgs(argv, args, parent, true)
try {
this.getArgs(argv, args, parent, true)
} catch (e) {
const message = getErrorMessage(e)
console.error(format(message, { color: 'red' }))
}
}
private parseOption(arg: string, argv: string[]) {
@@ -409,8 +429,7 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject> {
// default option - passes arg value even without flag name
const defaultOption = this.options.find((o) => o.isDefault)
if (defaultOption) {
_argv = this.parseOption(`--${defaultOption.name}`, [arg, ..._argv])
_argv.shift()
this.parseOption(`--${defaultOption.name}`, [arg])
continue
}
// not parsed by any step, add to extra key
@@ -466,7 +485,7 @@ export class MassargHelpCommand<
const _config = CommandConfig(z.any()).parse({
name: 'help',
aliases: ['h'],
description: 'Print help for this command, or a subcommand if specified',
description: 'Print help for this command, or a sub-command if specified',
run: (args: { command?: string }, parent) => {
if (args.command) {
const command = parent.commands.find((c) => c.name === args.command)

View File

@@ -394,6 +394,7 @@ function generateHelpTable<T extends GenerateTableCommandConfig | GenerateTableO
aliasPrefix,
negatePrefix,
negateAliasPrefix,
displayNegations,
}),
)
.filter((r) => !r.hidden)
@@ -422,6 +423,7 @@ function generateHelpTable<T extends GenerateTableCommandConfig | GenerateTableO
}
currentRow += `${word} `
}
subRows.push(currentRow)
if (!compact) {
subRows.push('')

View File

@@ -16,8 +16,7 @@ const addCmd = massarg<{ component: string }>({
description: 'Add a component',
aliases: ['a'],
run: (opts, parser) => {
parser.printHelp()
console.log('Adding component', opts.component)
console.log('Adding component', opts)
},
})
.option({
@@ -25,6 +24,7 @@ const addCmd = massarg<{ component: string }>({
description:
'Component to add. Ut consectetur eu et occaecat enim magna amet eiusmod laboris deserunt proident culpa nulla ipsum adipiscing ullamco laboris sed est',
aliases: ['c'],
isDefault: true,
// aliases: "" as never,
})
.option({
@@ -81,6 +81,9 @@ const main = massarg<A>({
bindCommand: true,
headerText: 'This is a header',
footerText: 'This is a footer',
optionOptions: {
displayNegations: true,
},
})
.main((opts, parser) => {
console.log('Main command - printing all opts')
@@ -94,6 +97,13 @@ const main = massarg<A>({
name: 'bool',
description: 'Example boolean option',
aliases: ['b'],
negatable: true,
})
.option({
name: 'string',
description:
'Laborum qui ex do consectetur magna. Ex do consectetur magna officia, consequat. Magna officia consequat labore veniam proident exercitation occaecat. Consequat labore veniam proident exercitation occaecat. Veniam proident exercitation occaecat aliquip.',
aliases: ['s'],
})
.option({
name: 'number',

View File

@@ -1,6 +1,6 @@
import z from 'zod'
import { zodEnumFromObjKeys, strConcat } from './utils'
export { strConcat }
import { zodEnumFromObjKeys } from './utils'
export { strConcat, indent } from './utils'
export const ansiStyles = {
reset: '\x1b[0m',

View File

@@ -1,4 +1,5 @@
import z from 'zod'
import { ValidationError } from './error'
/** @internal */
export function setOrPush<T>(
@@ -113,3 +114,10 @@ export function toPascalCase(str: string): string {
.map((s) => s[0].toUpperCase() + s.slice(1))
.join('')
}
export function getErrorMessage(err: unknown): string {
if (err instanceof Error) {
return err.message
}
return String(err)
}

View File

@@ -87,6 +87,24 @@ describe('prints help from option', () => {
command2.parse(['--help'])
expect(mainCmd).not.toHaveBeenCalled()
})
test('when default option exists', () => {
const command = massarg(opts)
.option({
name: 'test',
aliases: ['t'],
description: 'test',
isDefault: true,
})
.help({
bindOption: true,
})
const log = jest.spyOn(console, 'log').mockImplementation((...a) => {
console.info(...a)
})
command.parse(['--help'])
expect(log).toHaveBeenCalled()
})
})
test('help string', () => {