Skip to content
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

Fix #1156, make HelpCommand respect subcommands case-sensitivity and abbreviation #1172

Merged
merged 10 commits into from
Sep 12, 2020
38 changes: 31 additions & 7 deletions src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,10 @@ public CommandLine addSubcommand(String name, Object command, String... aliases)
* @since 0.9.7
*/
public Map<String, CommandLine> getSubcommands() {
return new LinkedHashMap<String, CommandLine>(getCommandSpec().subcommands());
CaseAwareLinkedMap<String, CommandLine> subcommands = new CaseAwareLinkedMap<String, CommandLine>();
subcommands.setCaseInsensitive(isSubcommandsCaseInsensitive());
subcommands.putAll(getCommandSpec().subcommands());
NewbieOrange marked this conversation as resolved.
Show resolved Hide resolved
return subcommands;
}
/**
* Returns the command that this is a subcommand of, or {@code null} if this is a top-level command.
Expand Down Expand Up @@ -5557,24 +5560,34 @@ public int size() {

private final LinkedHashMap<K, V> targetMap = new LinkedHashMap<K, V>();
private final HashMap<K, K> keyMap = new HashMap<K, K>();
private final Locale locale;
private final Set<K> keySet;
private final Set<K> keySet = new CaseAwareKeySet();
private boolean caseInsensitive = false;
private final Locale locale;

/**
* Constructs an empty LinkedCaseAwareMap instance with {@link java.util.Locale#ENGLISH}.
* Constructs an empty {@code CaseAwareLinkedMap} instance with {@link java.util.Locale#ENGLISH}.
*/
public CaseAwareLinkedMap() {
this(ENGLISH);
}

/**
* Constructs an empty LinkedCaseAwareMap instance with the specified {@link java.util.Locale}.
* Constructs an empty {@code CaseAwareLinkedMap} instance with the specified {@link java.util.Locale}.
* @param locale the locale to convert character cases
*/
public CaseAwareLinkedMap(Locale locale) {
this.locale = locale;
this.keySet = new CaseAwareKeySet();
}

/**
* Constructs a {@code CaseAwareLinkedMap} instance with the same mappings as the specified map.
* The {@code CaseAwareLinkedMap} instance is created with a default locale {@link java.util.Locale#ENGLISH}.
* @param map the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public CaseAwareLinkedMap(Map<? extends K, ? extends V> map) {
this(ENGLISH);
NewbieOrange marked this conversation as resolved.
Show resolved Hide resolved
putAll(map);
}

static boolean isCaseConvertible(Class<?> clazz) {
Expand Down Expand Up @@ -5612,6 +5625,11 @@ public void setCaseInsensitive(boolean caseInsensitive) {
this.caseInsensitive = caseInsensitive;
}

/** Returns the locale of the map. */
public Locale getLocale() {
return locale;
}

/**
* Returns the case-sensitive key of the specified case-insensitive key if {@code isCaseSensitive()}.
* Otherwise, the specified case-insensitive key is returned.
Expand Down Expand Up @@ -13924,7 +13942,13 @@ public void run() {
if (parent == null) { return; }
Help.ColorScheme colors = colorScheme != null ? colorScheme : Help.defaultColorScheme(ansi);
if (commands.length > 0) {
CommandLine subcommand = parent.getSubcommands().get(commands[0]);
Map<String, CommandLine> parentSubcommands = parent.getCommandSpec().subcommands();
String fullName = commands[0];
if (parent.isAbbreviatedSubcommandsAllowed()) {
fullName = AbbreviationMatcher.match(parentSubcommands.keySet(), fullName,
parent.isSubcommandsCaseInsensitive(), self);
}
CommandLine subcommand = parentSubcommands.get(fullName);
NewbieOrange marked this conversation as resolved.
Show resolved Hide resolved
if (subcommand != null) {
if (outWriter != null) {
subcommand.usage(outWriter, colors);
Expand Down
22 changes: 22 additions & 0 deletions src/test/java/picocli/CaseAwareLinkedMapTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
import org.junit.rules.TestRule;
import picocli.CommandLine.Model.CaseAwareLinkedMap;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import static org.junit.Assert.*;

public class CaseAwareLinkedMapTest {
Expand All @@ -23,6 +27,24 @@ public void testDefaultCaseSensitivity() {
assertFalse(new CaseAwareLinkedMap<String, String>().isCaseInsensitive());
}

@Test
public void testDefaultLocale() {
assertEquals(Locale.ENGLISH, new CaseAwareLinkedMap<String, String>().getLocale());
}

@Test
public void testCopyConstructor() {
Map<String, String> map = new HashMap<String, String>();
map.put("foo", "bar");
map.put("FOO", "BAR");
CaseAwareLinkedMap<String, String> copy = new CaseAwareLinkedMap<String, String>(map);
assertFalse(copy.isCaseInsensitive());
assertEquals(Locale.ENGLISH, copy.getLocale());
assertEquals(2, copy.size());
assertEquals("bar", copy.get("foo"));
assertEquals("BAR", copy.get("FOO"));
}

@Test
public void testCaseSensitiveAddDuplicateElement() {
CaseAwareLinkedMap<String, String> map = new CaseAwareLinkedMap<String, String>();
Expand Down
15 changes: 15 additions & 0 deletions src/test/java/picocli/CommandLineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1855,6 +1855,21 @@ public void testCommandListReturnsRegisteredCommands() {
assertTrue("cmd2", commandMap.get("cmd2").getCommand() instanceof Command2);
}

@Test
public void testCommandListReturnsCaseInsensitiveRegisteredCommands() {
@Command class MainCommand {}
@Command class Command1 {}
@Command class Command2 {}
CommandLine commandLine = new CommandLine(new MainCommand());
commandLine.addSubcommand("cmd1", new Command1()).addSubcommand("cmd2", new Command2());
commandLine.setSubcommandsCaseInsensitive(true);

Map<String, CommandLine> commandMap = commandLine.getSubcommands();
assertEquals(2, commandMap.size());
assertTrue("cmd1", commandMap.get("CMD1").getCommand() instanceof Command1);
assertTrue("cmd2", commandMap.get("CMD2").getCommand() instanceof Command2);
}

@Test(expected = InitializationException.class)
public void testPopulateCommandRequiresAnnotatedCommand() {
class App { }
Expand Down
84 changes: 81 additions & 3 deletions src/test/java/picocli/HelpSubCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ public void run() { }
assertEquals(String.format("Hi, colorScheme.ansi is OFF%n"), systemOutRule.getLog());
assertEquals(String.format("Hello, colorScheme.ansi is OFF%n"), systemErrRule.getLog());
}

@Test
public void testHelpSubcommandWithValidCommand() {
@Command(subcommands = {HelpTest.Sub.class, HelpCommand.class})
Expand All @@ -232,6 +233,61 @@ class App implements Runnable{ public void run(){}}
assertEquals(expected, sw.toString());
}

@Test
public void testHelpSubcommandWithCaseInsensitiveValidCommand() {
@Command(subcommands = {HelpTest.Sub.class, HelpCommand.class})
class App implements Runnable{ public void run(){}}

StringWriter sw = new StringWriter();
new CommandLine(new App())
.setOut(new PrintWriter(sw))
.setColorScheme(Help.defaultColorScheme(Help.Ansi.OFF))
.setSubcommandsCaseInsensitive(true)
.execute("help", "SUB");

String expected = String.format("" +
"Usage: <main class> sub%n" +
"This is a subcommand%n");
assertEquals(expected, sw.toString());
}

@Test
public void testHelpSubcommandWithAbbreviatedValidCommand() {
@Command(subcommands = {HelpTest.Sub.class, HelpCommand.class})
class App implements Runnable{ public void run(){}}

StringWriter sw = new StringWriter();
new CommandLine(new App())
.setOut(new PrintWriter(sw))
.setColorScheme(Help.defaultColorScheme(Help.Ansi.OFF))
.setAbbreviatedSubcommandsAllowed(true)
.execute("help", "s");

String expected = String.format("" +
"Usage: <main class> sub%n" +
"This is a subcommand%n");
assertEquals(expected, sw.toString());
}

@Test
public void testHelpSubcommandWithAbbreviatedCaseInsensitiveValidCommand() {
@Command(subcommands = {HelpTest.Sub.class, HelpCommand.class})
class App implements Runnable{ public void run(){}}

StringWriter sw = new StringWriter();
new CommandLine(new App())
.setOut(new PrintWriter(sw))
.setColorScheme(Help.defaultColorScheme(Help.Ansi.OFF))
.setAbbreviatedSubcommandsAllowed(true)
.setSubcommandsCaseInsensitive(true)
.execute("help", "S");

String expected = String.format("" +
"Usage: <main class> sub%n" +
"This is a subcommand%n");
assertEquals(expected, sw.toString());
}

@Test
public void testHelpSubcommandWithInvalidCommand() {
@Command(mixinStandardHelpOptions = true, subcommands = {HelpTest.Sub.class, HelpCommand.class})
Expand All @@ -254,6 +310,28 @@ class App implements Runnable{ public void run(){}}
assertEquals(expected, sw.toString());
}

@Test
public void testHelpSubcommandWithCaseSensitiveInvalidCommand() {
@Command(mixinStandardHelpOptions = true, subcommands = {HelpTest.Sub.class, HelpCommand.class})
class App implements Runnable{ public void run(){}}

StringWriter sw = new StringWriter();
new CommandLine(new App())
.setErr(new PrintWriter(sw))
.setColorScheme(Help.defaultColorScheme(Help.Ansi.OFF))
.execute("help", "SUB");

String expected = String.format("" +
"Unknown subcommand 'SUB'.%n" +
"Usage: <main class> [-hV] [COMMAND]%n" +
" -h, --help Show this help message and exit.%n" +
" -V, --version Print version information and exit.%n" +
"Commands:%n" +
" sub This is a subcommand%n" +
" help Displays help information about the specified command%n");
assertEquals(expected, sw.toString());
}

@Test
public void testHelpSubcommandWithHelpOption() {
@Command(subcommands = {HelpTest.Sub.class, HelpCommand.class})
Expand Down Expand Up @@ -392,7 +470,7 @@ public void testUsageTextWithHiddenSubcommand() {
}

@Test
public void testUsage_NoHeaderIfAllSubcommandHidden() {
public void testUsageNoHeaderIfAllSubcommandHidden() {
@Command(name = "foo", description = "This is a foo sub-command", hidden = true) class Foo { }
@Command(name = "bar", description = "This is a foo sub-command", hidden = true) class Bar { }
@Command(name = "app", abbreviateSynopsis = true) class App { }
Expand All @@ -410,7 +488,7 @@ public void testUsage_NoHeaderIfAllSubcommandHidden() {
}

@Test
public void testHelp_allSubcommands() {
public void testHelpAllSubcommands() {
@Command(name = "foo", description = "This is a visible subcommand") class Foo { }
@Command(name = "bar", description = "This is a hidden subcommand", hidden = true) class Bar { }
@Command(name = "app", subcommands = {Foo.class, Bar.class}) class App { }
Expand All @@ -423,4 +501,4 @@ public void testHelp_allSubcommands() {
assertEquals(1, help.subcommands().size());
assertEquals(new HashSet<String>(Arrays.asList("foo")), help.subcommands().keySet());
}
}
}