Fix did-you-mean system
This commit is contained in:
1 parent
9bf52513fb
commit
e878bdaf5a
2 files changed
+47
-6
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
# CLIforge
|
# CLIForge
|
||||||
|
|
||||||
A header-only C++20 engine for building CLIs whose commands are ordinary
|
A header-only C++20 engine for building CLIs whose commands are ordinary
|
||||||
free functions. Parameters and options are explicitly typed where
|
free functions. Parameters and options are explicitly typed where
|
||||||
@@ -158,6 +158,26 @@ three, clearly separated, rather than silently picking one. A
|
|||||||
fully-specified prefix (`project myapp delete --help`) narrows to the
|
fully-specified prefix (`project myapp delete --help`) narrows to the
|
||||||
one actual match, exactly as before.
|
one actual match, exactly as before.
|
||||||
|
|
||||||
|
The same treatment applies to an *incomplete* command that wasn't
|
||||||
|
explicitly asking for help: `packctl project` alone is missing
|
||||||
|
`<name>`, and that's equally true of all three `project` commands (they
|
||||||
|
report the identical error). Rather than arbitrarily surfacing whichever
|
||||||
|
one happened to be registered first, every command tied for "most
|
||||||
|
specific incomplete match" is listed:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ packctl project
|
||||||
|
Error: missing required argument '<name>': expected a value of type string
|
||||||
|
'project' matches 3 commands:
|
||||||
|
packctl project <name> create [OPTIONS] Scaffold a new project from a template
|
||||||
|
packctl project <name> delete [OPTIONS] Permanently delete a project by name
|
||||||
|
packctl project <name> [OPTIONS] Show information about a project
|
||||||
|
```
|
||||||
|
|
||||||
|
A single most-specific match (e.g. `packctl project x create --template`,
|
||||||
|
missing only `--template`'s value) still reports just the one relevant
|
||||||
|
`Usage:` line, unchanged.
|
||||||
|
|
||||||
## Keeping allocations down
|
## Keeping allocations down
|
||||||
|
|
||||||
* `TypeInfo`'s parser is a **plain stateless function pointer**, not
|
* `TypeInfo`'s parser is a **plain stateless function pointer**, not
|
||||||
|
|||||||
@@ -81,26 +81,47 @@ public:
|
|||||||
};
|
};
|
||||||
std::vector<Candidate> candidates;
|
std::vector<Candidate> candidates;
|
||||||
std::string bestPartialError;
|
std::string bestPartialError;
|
||||||
Command* bestPartialCmd = nullptr;
|
std::vector<Command*> bestPartialCmds; // every command tied for the best partial score
|
||||||
int bestPartialScore = -1;
|
int bestPartialScore = -1;
|
||||||
for (auto& cmd : commands_) {
|
for (auto& cmd : commands_) {
|
||||||
StructuredMatch m = cmd->tryMatchStructured(tokens);
|
StructuredMatch m = cmd->tryMatchStructured(tokens);
|
||||||
if (m.success) {
|
if (m.success) {
|
||||||
candidates.push_back({cmd.get(), std::move(m)});
|
candidates.push_back({cmd.get(), std::move(m)});
|
||||||
} else if (!m.error.empty() && m.keywordScore > bestPartialScore) {
|
} else if (!m.error.empty()) {
|
||||||
|
if (m.keywordScore > bestPartialScore) {
|
||||||
bestPartialScore = m.keywordScore;
|
bestPartialScore = m.keywordScore;
|
||||||
bestPartialError = m.error;
|
bestPartialError = m.error;
|
||||||
bestPartialCmd = cmd.get();
|
bestPartialCmds.clear();
|
||||||
|
bestPartialCmds.push_back(cmd.get());
|
||||||
|
} else if (m.keywordScore == bestPartialScore) {
|
||||||
|
// Genuinely tied -- e.g. "project" alone is an equally
|
||||||
|
// incomplete prefix of create/delete/info, all of which
|
||||||
|
// report the identical "missing <name>" error. Rather
|
||||||
|
// than silently picking whichever was registered first
|
||||||
|
// (which read as an arbitrary, misleading suggestion),
|
||||||
|
// remember all of them and show every option.
|
||||||
|
bestPartialCmds.push_back(cmd.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidates.empty()) {
|
if (candidates.empty()) {
|
||||||
if (bestPartialCmd) {
|
if (!bestPartialCmds.empty()) {
|
||||||
// Every keyword matched some command, just with a bad or
|
// Every keyword matched some command, just with a bad or
|
||||||
// missing argument value -- this is much more actionable
|
// missing argument value -- this is much more actionable
|
||||||
// than a generic "no command matches" + fuzzy suggestions.
|
// than a generic "no command matches" + fuzzy suggestions.
|
||||||
std::cerr << "Error: " << bestPartialError << "\n";
|
std::cerr << "Error: " << bestPartialError << "\n";
|
||||||
std::cerr << "Usage: " << programName_ << " " << bestPartialCmd->usageLine() << "\n";
|
if (bestPartialCmds.size() == 1) {
|
||||||
|
std::cerr << "Usage: " << programName_ << " " << bestPartialCmds.front()->usageLine()
|
||||||
|
<< "\n";
|
||||||
|
} else {
|
||||||
|
std::cerr << "'" << join(tokens) << "' matches " << bestPartialCmds.size()
|
||||||
|
<< " commands:\n";
|
||||||
|
for (auto* cmd : bestPartialCmds) {
|
||||||
|
std::string line = programName_ + " " + cmd->usageLine();
|
||||||
|
std::cerr << " " << padRight(line, 46) << firstLine(cmd->description()) << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
std::cerr << "Error: no command matches '" << join(tokens) << "'.\n";
|
std::cerr << "Error: no command matches '" << join(tokens) << "'.\n";
|
||||||
|
|||||||
Reference in new issue
Block a user