diff --git a/src/main.ts b/src/main.ts index 480764ba..34900b57 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import * as Config from '@oclif/config' import Help from '@oclif/plugin-help' import {Command} from '.' +import {splitArgv} from './util' export class Main extends Command { static run(argv = process.argv.slice(2), options?: Config.LoadOptions) { @@ -15,7 +16,7 @@ export class Main extends Command { } async run() { - let [id, ...argv] = this.argv + const {id, argv} = splitArgv(this.argv, this.config.commandIDs) this.parse({strict: false, '--': false, ...this.ctor as any}) if (!this.config.findCommand(id)) { let topic = this.config.findTopic(id) diff --git a/src/util.ts b/src/util.ts index 45f7b2bf..0431a11b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,36 @@ export function compact(a: (T | undefined)[]): T[] { return a.filter((a): a is T => !!a) } + +/** + * Splits argv to command ID and the rest of the array. + * + * For space-separated subcommands, the ID is built from multiple items in + * the source array. For colon-separated subcommands, the argv item is the ID. + */ +export function splitArgv(argv: string[], commandIDs: string[]) { + let argvIndex = 0 + let id = '' + let idCandidate = argv[argvIndex] + + while (commandIDs.includes(idCandidate) || !id) { + if (commandIDs.includes(idCandidate)) { + id = idCandidate + } + argvIndex++ + if (argvIndex > argv.length) { + break + } + + idCandidate += ` ${argv[argvIndex]}` + } + + if (id === '') { + throw new Error('Command ID not found') + } + + return { + id, + argv: argv.slice(argvIndex), + } +} diff --git a/test/util.test.ts b/test/util.test.ts new file mode 100644 index 00000000..879bb875 --- /dev/null +++ b/test/util.test.ts @@ -0,0 +1,61 @@ +import {expect} from 'fancy-test' + +import {splitArgv} from '../src/util' + +describe('splitArgv', () => { + it('Colon support (no breaking change)', () => { + expectEqual( + splitArgv(['user:add', 'Peter'], ['user:add']), + {id: 'user:add', argv: ['Peter']} + ) + }) + + it('Space-separated when topic itself is in the list', () => { + expectEqual( + splitArgv(['user', 'add', 'Peter'], ['user', 'user add']), + {id: 'user add', argv: ['Peter']} + ) + }) + + it('Space-separated when topic is missing from the list', () => { + expectEqual( + splitArgv(['user', 'add', 'Peter'], ['user add']), + {id: 'user add', argv: ['Peter']} + ) + }) + + it('Similar but not really - topic level', () => { + expect(() => splitArgv(['user', 'add', 'Peter'], ['user2'])).to.throw(Error) + }) + + it('Similar but not really - nested level', () => { + expect(() => splitArgv(['user', 'add', 'Peter'], ['user add2'])).to.throw(Error) + }) + + it('Multiple nesting levels', () => { + expectEqual( + splitArgv(['user', 'config', 'frontend', 'add', 'key', 'value'], ['user config frontend add']), + {id: 'user config frontend add', argv: ['key', 'value']} + ) + }) + + it('Multiple nesting levels, ending in the middle', () => { + expectEqual( + splitArgv(['user', 'config', 'frontend', 'add', 'key', 'value'], ['user config frontend']), + {id: 'user config frontend', argv: ['add', 'key', 'value']} + ) + }) + + it('Flags', () => { + expectEqual( + splitArgv(['user', 'add', '--name', 'Peter'], ['user add']), + {id: 'user add', argv: ['--name', 'Peter']} + ) + }) + +}) + +// Small strongly-typed helper +function expectEqual(expected: ReturnType, actual: ReturnType) { + expect(expected).to.deep.equal(actual) +}