Repository: oclif/core
Pull request #143 (2021-04-09 15:07:04)

feat: support misspelled spaced commands
Add support for misspelled spaced commands

codecov[bot]: # [Codecov](https://codecov.io/gh/oclif/core/pull/143?src=pr&el=h1) Report
> Merging [#143](https://codecov.io/gh/oclif/core/pull/143?src=pr&el=desc) (3594e57) into [master](https://codecov.io/gh/oclif/core/commit/abd10fdbb313c25170f4492cc8dfea8ffa3a9928?el=desc) (abd10fd) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/oclif/core/pull/143/graphs/tree.svg?width=650&height=150&src=pr&token=K0kGk9tefA)](https://codecov.io/gh/oclif/core/pull/143?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master    #143   +/-   ##
======================================
  Coverage    0.00%   0.00%           
======================================
  Files           3       3           
  Lines          15      15           
  Branches        2       2           
======================================
  Misses         15      15           
```


| [Impacted Files](https://codecov.io/gh/oclif/core/pull/143?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/index.ts](https://codecov.io/gh/oclif/core/pull/143/diff?src=pr&el=tree#diff-c3JjL2luZGV4LnRz) | `0.00% <ø> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/oclif/core/pull/143?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/oclif/core/pull/143?src=pr&el=footer). Last update [abd10fd...3594e57](https://codecov.io/gh/oclif/core/pull/143?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/config/config.ts b/src/config/config.ts
index 54991ef..3946478 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -246,7 +246,7 @@ export class Config implements IConfig {
     debug('runCommand %s %o', id, argv)
     const c = cachedCommand || this.findCommand(id)
     if (!c) {
-      await this.runHook('command_not_found', {id})
+      await this.runHook('command_not_found', {id, argv})
       throw new CLIError(`command ${id} not found`)
     }
     const command = c.load()
diff --git a/src/help/util.ts b/src/help/util.ts
index ce1aa65..3300a4c 100644
--- a/src/help/util.ts
+++ b/src/help/util.ts
@@ -45,14 +45,29 @@ function collateSpacedCmdIDFromArgs(argv: string[], config: IConfig): string[] {
 
   const ids = config.commandIDs.concat(config.topics.map(t => t.name))
 
-  const findId = (id: string, next: string[]): string | undefined => {
-    const idPresnet = (id: string) => ids.includes(id)
-    if (idPresnet(id) && !idPresnet(`${id}:${next[0]}`)) return id
-    if (next.length === 0 || next[0] === '--') return
-    return findId(`${id}:${next[0]}`, next.slice(1))
+  const findId = (argv: string[]): string | undefined => {
+    const final: string[] = []
+    const idPresent = (id: string) => ids.includes(id)
+    const isFlag = (s: string) => s.startsWith('-')
+    const finalizeId = (s?: string) => s ? [...final, s].join(':') : final.join(':')
+
+    const hasSubCommandsWithArgs = () => {
+      const subCommands = config.commands.filter(c => (c.id).startsWith(finalizeId()))
+      return Boolean(subCommands.find(cmd => cmd.args.length > 0))
+    }
+
+    for (const arg of argv) {
+      if (idPresent(finalizeId(arg))) final.push(arg)
+      // If the parent topic has a command that expects positional arguments, then we cannot
+      // assume that any subsequent string could be part of the command name
+      else if (isFlag(arg) || hasSubCommandsWithArgs()) break
+      else final.push(arg)
+    }
+
+    return finalizeId()
   }
 
-  const id = findId(argv[0], argv.slice(1))
+  const id = findId(argv)
 
   if (id) {
     const argvSlice = argv.slice(id.split(':').length)
@@ -62,9 +77,17 @@ function collateSpacedCmdIDFromArgs(argv: string[], config: IConfig): string[] {
   return argv // ID is argv[0]
 }
 
+export function toStandardizedId(commandID: string, config: IConfig): string {
+  return commandID.replace(new RegExp(config.topicSeparator, 'g'), ':')
+}
+
+export function toConfiguredId(commandID: string, config: IConfig): string {
+  return commandID.replace(new RegExp(':', 'g'), config.topicSeparator)
+}
+
 export function standarizeIDFromArgv(argv: string[], config: IConfig): string[] {
   if (argv.length === 0) return argv
   if (config.topicSeparator === ' ') argv = collateSpacedCmdIDFromArgs(argv, config)
-  else if (config.topicSeparator !== ':') argv[0] = argv[0].replace(new RegExp(config.topicSeparator, 'g'), ':')
+  else if (config.topicSeparator !== ':') argv[0] = toStandardizedId(argv[0], config)
   return argv
 }
diff --git a/src/index.ts b/src/index.ts
index 21177fd..3fcedf9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,7 +8,9 @@ import * as Interfaces from './interfaces'
 import * as Errors from './errors'
 import * as Flags from './flags'
 import {HelpBase, Help, getHelpClass} from './help'
+import {toStandardizedId, toConfiguredId} from './help/util'
 import * as Parser from './parser'
+import {Hook} from './interfaces/hooks'
 
 export {
   Command,
@@ -18,12 +20,15 @@ export {
   getHelpClass,
   Help,
   HelpBase,
+  Hook,
   Interfaces,
   Parser,
   Plugin,
   run,
   toCached,
   tsPath,
+  toStandardizedId,
+  toConfiguredId,
 }
 
 function checkCWD() {
diff --git a/src/interfaces/hooks.ts b/src/interfaces/hooks.ts
index e45bfcf..dfeeebc 100644
--- a/src/interfaces/hooks.ts
+++ b/src/interfaces/hooks.ts
@@ -18,7 +18,7 @@ export interface Hooks {
   };
   preupdate: {channel: string};
   update: {channel: string};
-  'command_not_found': {id: string};
+  'command_not_found': {id: string; argv?: string[]};
   'plugins:preinstall': {
     plugin: {
       name: string;


Test patch:
diff --git a/test/help/util.test.ts b/test/help/util.test.ts
index dc9381c..5ea2fa0 100644
--- a/test/help/util.test.ts
+++ b/test/help/util.test.ts
@@ -2,7 +2,7 @@
 import {resolve} from 'path'
 import {Config, Interfaces} from '../../src'
 import {expect, test} from '@oclif/test'
-import {getHelpClass} from '../../src/help'
+import {getHelpClass, standarizeIDFromArgv} from '../../src/help'
 import configuredHelpClass from  '../../src/help/_test-help-class'
 
 describe('util', () => {
@@ -41,4 +41,29 @@ describe('util', () => {
       })
     })
   })
+
+  describe('#standarizeIDFromArgv', () => {
+    test
+    .it('should return standardized id when topic separator is a colon', () => {
+      config.pjson.oclif.topicSeparator = ':'
+      const actual = standarizeIDFromArgv(['foo:bar', '--baz'], config)
+      expect(actual).to.deep.equal(['foo:bar', '--baz'])
+    })
+
+    test
+    .stub(Config.prototype, 'commandIDs', () => ['foo', 'foo:bar'])
+    .it('should return standardized id when topic separator is a space', () => {
+      config.topicSeparator = ' '
+      const actual = standarizeIDFromArgv(['foo', 'bar', '--baz'], config)
+      expect(actual).to.deep.equal(['foo:bar', '--baz'])
+    })
+
+    test
+    .stub(Config.prototype, 'commandIDs', () => ['foo', 'foo:bar'])
+    .it('should return standardized id when topic separator is a space and command is misspelled', () => {
+      config.topicSeparator = ' '
+      const actual = standarizeIDFromArgv(['foo', 'ba', '--baz'], config)
+      expect(actual).to.deep.equal(['foo:ba', '--baz'])
+    })
+  })
 })