-
Notifications
You must be signed in to change notification settings - Fork 428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support unambiguous abbreviations #732
Labels
Milestone
Comments
This is a duplicate of #10, but your description is much better, so I’ll keep both tickets open. Contributions (code snippets, PRs, tests, etc) welcome! Note to self: import org.junit.Test;
import static org.junit.Assert.*;
import java.util.*;
public class PrefixTest {
static List<String> splitIntoChunks(String command) {
List<String> result = new ArrayList<String>();
int start = 0;
for (int i = 0; i < command.length(); i++) {
int codepoint = command.codePointAt(i);
if (Character.isUpperCase(codepoint) || '-' == codepoint) {
String chunk = makeCanonical(command.substring(start, i));
if (chunk.length() > 0) {
result.add(chunk);
}
start = i;
}
}
if (start < command.length()) {
String chunk = makeCanonical(command.substring(start));
if (chunk.length() > 0) {
result.add(chunk);
}
}
return result;
}
private static String makeCanonical(String str) {
if ("-".equals(str)) {
return "";
}
if (str.startsWith("-") && str.length() > 1) {
int codepoint = str.codePointAt(1);
return ((char) Character.toUpperCase(codepoint)) + str.substring(1 + Character.charCount(codepoint));
}
return str;
}
static String match(Map<String, String> map, String abbreviation) {
if (map.containsKey(abbreviation)) {
return abbreviation;// was: return map.get(abbreviation);
}
List<String> abbreviatedKeyChunks = splitIntoChunks(abbreviation);
List<String> candidates = new ArrayList<String>();
next_key: for (String key : map.keySet()) {
List<String> keyChunks = splitIntoChunks(key);
if (abbreviatedKeyChunks.size() <= keyChunks.size() && keyChunks.get(0).startsWith(abbreviatedKeyChunks.get(0))) { // first chunk must match
int matchCount = 1;
int keyChunk = 1;
for (int i = 1; i < abbreviatedKeyChunks.size(); i++) {
boolean found = false;
for (int j = keyChunk; j < keyChunks.size(); j++) {
if (keyChunks.get(j).startsWith(abbreviatedKeyChunks.get(i))) { // first chunk must match
keyChunk = j + 1;
found = true;
break;
}
}
if (!found) { // not a candidate
continue next_key;
}
matchCount++;
}
if (matchCount == abbreviatedKeyChunks.size()) {
candidates.add(key);
}
}
}
if (candidates.size() > 1) {
String str = candidates.toString();
throw new IllegalArgumentException(abbreviation + " is not unique: it matches '" + str.substring(1, str.length() - 1).replace(", ", "', '") + "'");
}
return candidates.isEmpty() ? null : candidates.get(0);
}
private Map<String, String> createMap() {
Map<String, String> result = new LinkedHashMap<String, String>();
result.put("kebab-case-extra", "kebab-case-extra");
result.put("kebab-case-extra-extra", "kebab-case-extra-extra");
result.put("kebab-case", "kebab-case");
result.put("kc", "kebab-case"); // alias
result.put("very-long-kebab-case", "very-long-kebab-case");
result.put("camelCase", "camelCase");
result.put("veryLongCamelCase", "veryLongCamelCase");
return result;
}
@Test
public void testPrefixMatch() {
Map<String, String> map = createMap();
assertEquals("kebab-case", match(map, "kebab-case"));
assertEquals("kebab-case-extra", match(map, "kebab-case-extra"));
assertEquals("very-long-kebab-case", match(map, "very-long-kebab-case"));
assertEquals("very-long-kebab-case", match(map, "v-l-k-c"));
assertEquals("very-long-kebab-case", match(map, "vLKC"));
assertEquals("camelCase", match(map, "camelCase"));
assertEquals("camelCase", match(map, "cC"));
assertEquals("camelCase", match(map, "c-c"));
assertEquals("camelCase", match(map, "camC"));
assertEquals("veryLongCamelCase", match(map, "veryLongCamelCase"));
assertEquals("veryLongCamelCase", match(map, "vLCC"));
assertEquals("veryLongCamelCase", match(map, "v-l-c-c"));
try {
match(map, "vLC");
fail("Expected exception");
} catch (IllegalArgumentException ex) {
assertEquals("vLC is not unique: it matches 'very-long-kebab-case', 'veryLongCamelCase'", ex.getMessage());
}
try {
match(map, "k-c");
fail("Expected exception");
} catch (IllegalArgumentException ex) {
assertEquals("k-c is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
}
try {
match(map, "kC");
fail("Expected exception");
} catch (IllegalArgumentException ex) {
assertEquals("kC is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
}
try {
match(map, "keb-ca");
fail("Expected exception");
} catch (IllegalArgumentException ex) {
assertEquals("keb-ca is not unique: it matches 'kebab-case-extra', 'kebab-case-extra-extra', 'kebab-case'", ex.getMessage());
}
}
@Test
public void testSplitIntoChunks() {
assertEquals(Arrays.asList("k", "C"), splitIntoChunks("kC"));
assertEquals(Arrays.asList("k", "C"), splitIntoChunks("k-c"));
assertEquals(Arrays.asList("kebab", "Case"), splitIntoChunks("kebab-case"));
assertEquals(Arrays.asList("very", "Long", "Kebab", "Case"), splitIntoChunks("very-long-kebab-case"));
assertEquals(Arrays.asList("camel", "Case"), splitIntoChunks("camelCase"));
assertEquals(Arrays.asList("very", "Long", "Camel", "Case"), splitIntoChunks("veryLongCamelCase"));
}
} |
This was referenced May 15, 2020
remkop
added a commit
that referenced
this issue
May 23, 2020
remkop
added a commit
that referenced
this issue
May 23, 2020
remkop
added a commit
that referenced
this issue
May 24, 2020
remkop
added a commit
that referenced
this issue
May 24, 2020
remkop
added a commit
that referenced
this issue
May 24, 2020
remkop
added a commit
that referenced
this issue
May 24, 2020
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Some programs allow subcommands to be abbreviated with any unambiguous abbreviation of the subcommand name. I would like to request the ability to allow this in picocli.
The key here is that these are not explicitly listed abbreviations, but rather any unambiguous abbreviation of the command name or possibly even its aliases.
For example, in Mercurial,
hg showconfig
can be run withhg showc
,hg show
orhg sho
. Howeverhg sh
andhg s
are ambiguous and generate the following errors. (At least they do with the set of Mercurial plugins I have installed. Yours may be different.)Another example is the
btrfs
filesystem command in whichbtrfs fi usa /
andbtrfs f u /
are the same asbtrfs filesystem usage /
.This concept could also extend to option names. For example Python's
argparse
, supports this with theallow_abbrev
setting.In the cases I have seen this implemented, "unambiguous abbreviation" really means "unambiguous prefix", but there might be generalizations. (This is not critical to me. It is just a possible point in the design space.) For example, you might allow "foo-bar" to be abbreviated as "f-b" (under the rule that "-" separated words can be abbreviated individually) or "fb" or "fr" (under the rule that abbreviations are subsequences). The latter might be impractical to use (e.g., "fr" becomes ambiguous between "free" and "foo-bar"). I only mention it as it is what is used by Emacs Helm, albeit for interactive input not command lines.
The text was updated successfully, but these errors were encountered: