feature: suggest nearest match if user give unrecognized option

similar to git `Did you mean '...'?`, this feature put `std/editDistance`
into action help users to quickly recognize his error instead of plain
`Unrecognized option '...'.`
This commit is contained in:
jangko 2021-11-02 18:04:48 +07:00
parent 7176de4ddb
commit 5e732379f4
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
1 changed files with 39 additions and 2 deletions

View File

@ -1,5 +1,5 @@
import import
std/[options, strutils, wordwrap], std/[options, strutils, wordwrap, editdistance],
stew/shims/macros, stew/shims/macros,
confutils/[defs, cli_parser, config_file] confutils/[defs, cli_parser, config_file]
@ -409,6 +409,36 @@ proc findSubCmd(cmd: CmdInfo, name: string): CmdInfo =
return nil return nil
proc distanceOpt(opts: openarray[OptInfo], name: string): (OptInfo, int) =
# find nearest match using `editDistance`
if opts.len == 0:
return
var
currOpt = opts[0]
currDist = editDistance(currOpt.name, name)
for i in 1..<opts.len:
let distance = editDistance(opts[i].name, name)
if distance < currDist:
currOpt = opts[i]
currDist = distance
(currOpt, currDist)
proc distanceOpt(cmds: openarray[CmdInfo], name: string): OptInfo =
if cmds.len == 0:
return
var (currOpt, currDist) = distanceOpt(cmds[0].opts, name)
for i in 1..<cmds.len:
let (opt, distance) = distanceOpt(cmds[i].opts, name)
if distance < currDist:
currOpt = opt
currDist = distance
currOpt
proc startsWithIgnoreStyle(s: string, prefix: string): bool = proc startsWithIgnoreStyle(s: string, prefix: string): bool =
# Similar in spirit to cmpIgnoreStyle, but compare only the prefix. # Similar in spirit to cmpIgnoreStyle, but compare only the prefix.
var i = 0 var i = 0
@ -1002,11 +1032,18 @@ proc load*(Configuration: type,
opt = findOpt(defaultCmd.opts, key) opt = findOpt(defaultCmd.opts, key)
if opt != nil: if opt != nil:
activateCmd(subCmdDiscriminator, defaultCmd) activateCmd(subCmdDiscriminator, defaultCmd)
else:
# we will fail, but before that, suggest nearest match from defaultCmd too.
activeCmds.add defaultCmd
else: else:
discard discard
if opt != nil: if opt != nil:
applySetter(opt.idx, val) applySetter(opt.idx, val)
else:
let opt = distanceOpt(activeCmds, key) # suggest nearest match
if opt != nil:
fail "Unrecognized option '$1'. Did you mean '$2'?" % [key, opt.name]
else: else:
fail "Unrecognized option '$1'" % [key] fail "Unrecognized option '$1'" % [key]