Skip to content

Commit dbebf40

Browse files
committed
fix(cli): report mistyped subcommand before options
1 parent 4a39007 commit dbebf40

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

src/factory-core.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ export function defineGroup (config: GroupConfig, ...commands: OpaqueCommandHand
350350
const group = new Command(config.name)
351351
group.description(config.description)
352352
group.allowExcessArguments(true)
353+
// Defer unknown option validation until after Commander resolves a child.
354+
// Leaf commands still validate their own options during delegated parsing.
355+
group.allowUnknownOption(true)
353356
configureErrorOutput(group)
354357
configureJsonHelp(group)
355358

@@ -363,8 +366,12 @@ export function defineGroup (config: GroupConfig, ...commands: OpaqueCommandHand
363366

364367
// Default action: error on unknown sub-command, show help otherwise
365368
group.action(function (this: OpaqueCommandHandle) {
366-
if (this.args.length > 0) {
367-
group.error(`unknown command: ${this.args[0]}`)
369+
const firstArg = this.args[0]
370+
if (firstArg != null) {
371+
if (firstArg.startsWith('-')) {
372+
group.error(`unknown option '${firstArg}'`)
373+
}
374+
group.error(`unknown command: ${firstArg}`)
368375
} else {
369376
group.help()
370377
}

test/cli.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,64 @@ describe('elastic CLI -- stack command tree', () => {
334334
})
335335
})
336336

337+
describe('elastic CLI -- command and option error ordering', () => {
338+
it('reports an unknown subcommand before parsing its trailing options', async () => {
339+
const dir = await mkdtemp(join(tmpdir(), 'elastic-cli-unknown-command-'))
340+
await writeFile(join(dir, '.elasticrc.yml'), [
341+
'current_context: local',
342+
'contexts:',
343+
' local:',
344+
' elasticsearch:',
345+
' url: http://localhost:9200',
346+
'',
347+
].join('\n'))
348+
349+
try {
350+
const { code, stderr } = await runCli(
351+
['stack', 'es', 'serch', '--index', 'my-index'],
352+
{ cwd: dir, env: { HOME: dir, USERPROFILE: dir, XDG_CONFIG_HOME: dir } }
353+
)
354+
355+
assert.equal(code, 1)
356+
assert.match(stderr, /unknown command: serch/)
357+
assert.doesNotMatch(stderr, /unknown option/)
358+
} finally {
359+
await rm(dir, { recursive: true })
360+
}
361+
})
362+
363+
it('still reports an unknown option for a valid command', async () => {
364+
const dir = await mkdtemp(join(tmpdir(), 'elastic-cli-unknown-option-'))
365+
await writeFile(join(dir, '.elasticrc.yml'), [
366+
'current_context: local',
367+
'contexts:',
368+
' local:',
369+
' elasticsearch:',
370+
' url: http://localhost:9200',
371+
'',
372+
].join('\n'))
373+
374+
try {
375+
const { code, stderr } = await runCli(
376+
['stack', 'es', 'search', '--not-a-real-option'],
377+
{ cwd: dir, env: { HOME: dir, USERPROFILE: dir, XDG_CONFIG_HOME: dir } }
378+
)
379+
380+
assert.equal(code, 1)
381+
assert.match(stderr, /unknown option '--not-a-real-option'/)
382+
} finally {
383+
await rm(dir, { recursive: true })
384+
}
385+
})
386+
387+
it('reports an unknown option when no subcommand is provided', async () => {
388+
const { code, stderr } = await runCli(['sanitize', '--not-a-real-option'])
389+
390+
assert.equal(code, 1)
391+
assert.match(stderr, /unknown option '--not-a-real-option'/)
392+
})
393+
})
394+
337395
describe('elastic CLI -- --help --json', () => {
338396
it('`elastic --help --json` emits structured JSON help', async () => {
339397
const dir = await mkdtemp(join(tmpdir(), 'elastic-cli-help-json-'))

0 commit comments

Comments
 (0)