Closed charphi closed 6 years ago
That is a cool feature indeed! Do you think you can provide a pull request for this?
I will try to find some time to do it but I can't promise.
By the way, here is an algorithm that might be used to find proper matches: https://en.wikipedia.org/wiki/Bitap_algorithm
Nice, thanks for the link!
I've looked at the code and I'm not sure where to put changes without breaking the logic. Here is the code I came up so far:
private void internalHandleParseException(ParameterException ex, PrintStream out, Help.Ansi ansi, String[] args) {
if (ex instanceof UnmatchedArgumentException && !ex.getCommandLine().getSubcommands().isEmpty()) {
// PRINT SUGGESTION
String ummatchedParameter = null; // retrieve this from exception
out.println("Did you mean this?");
BitapFilter filter = new BitapFilter(ummatchedParameter, 1);
for (String subCommand : ex.getCommandLine().getSubcommands().keySet()) {
if (filter.test(subCommand)) {
out.println(subCommand);
}
}
} else {
out.println(ex.getMessage());
ex.getCommandLine().usage(out, ansi);
}
}
// https://en.wikipedia.org/wiki/Bitap_algorithm
static final class BitapFilter implements Predicate<String> {
private final int alphabetRange = 128;
private final long[] patternMask;
private final int patternLength;
private final int k;
public BitapFilter(String pattern, int k) {
/* Initialize the pattern bitmasks */
this.patternMask = new long[alphabetRange];
for (int i = 0; i < pattern.length(); ++i) {
patternMask[(int) pattern.charAt(i)] |= 1 << i;
}
this.patternLength = pattern.length();
this.k = k;
}
@Override
public boolean test(String text) {
/* Initialize the bit array R */
long[] r = new long[k + 1];
for (int i = 0; i <= k; i++) {
r[i] = 1;
}
/* Performs test */
for (int i = 0; i < text.length(); i++) {
long old = 0;
long nextOld = 0;
for (int d = 0; d <= k; ++d) {
// Three operations of the Levenshtein distance
long sub = (old | (r[d] & patternMask[text.charAt(i)])) << 1;
long ins = old | ((r[d] & patternMask[text.charAt(i)]) << 1);
long del = (nextOld | (r[d] & patternMask[text.charAt(i)])) << 1;
old = r[d];
r[d] = sub | ins | del | 1;
nextOld = r[d];
}
// When r[k] is full of zeros, it means we matched the pattern
// (modulo k errors)
if (0 < (r[k] & (1 << patternLength))) {
return true;
}
}
return false;
}
}
I haven't looked at this in detail yet, but perhaps the DefaultExceptionHandler would be a good place?
CosineSimilarity may also be interesting.
It is used like this:
context.console.error("Command not found ${commandName}")
def mostSimilar = CosineSimilarity.mostSimilar(commandName, commandsByName.keySet())
List<String> topMatches = mostSimilar.subList(0, Math.min(3, mostSimilar.size()));
if (topMatches) {
context.console.log("Did you mean: ${topMatches.join(' or ')}?")
}
This is fixed in the latest release https://github.com/remkop/picocli/releases/tag/v3.3.0 . Thanks for the suggestion!
You are welcome :) And thank you for implementing it. This will be really useful for someone like me who does typos all the time.
When you mistype a command in git command line, it suggests several possible commands instead of listing all. It would be interesting to have the same behaviour with picocli.
For example: