Skip to content
Merged
36 changes: 29 additions & 7 deletions src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ 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());
return new CaseAwareLinkedMap<String, CommandLine>(getCommandSpec().commands);
}
/**
* 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 +5557,35 @@ 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, case-sensitivity and locale as the specified map.
* @param map the map whose mappings, case-sensitivity and locale are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public CaseAwareLinkedMap(CaseAwareLinkedMap<? extends K, ? extends V> map) {
this.targetMap.putAll(map.targetMap);
this.keyMap.putAll(map.keyMap);
this.caseInsensitive = map.caseInsensitive;
this.locale = map.locale;
}

static boolean isCaseConvertible(Class<?> clazz) {
Expand Down Expand Up @@ -5612,6 +5623,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 +13940,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);
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() {
CaseAwareLinkedMap<String, String> map = new CaseAwareLinkedMap<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());
}
}
}