diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index 88d7fe1bd..d5f048a85 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -15334,9 +15334,29 @@ static class AutoHelpMixin { @Command(name = "help", header = "Display help information about the specified command.", synopsisHeading = "%nUsage: ", helpCommand = true, description = {"%nWhen no COMMAND is given, the usage help for the main command is displayed.", - "If a COMMAND is specified, the help for that command is shown.%n"}) + "If a COMMAND is specified, the help for that command is shown.%n"}, + preprocessor = HelpCommand.HelpPreprocessor.class) public static final class HelpCommand implements IHelpCommandInitializable, IHelpCommandInitializable2, Runnable { + /** Custom {@link IParameterPreprocessor} to greedily consume the {@link #commands} parameter + * when it matches a command, before repeatable subcommand processing consumes it instead. + */ + private static class HelpPreprocessor implements IParameterPreprocessor { + @Override + public boolean preprocess(Stack args, CommandSpec commandSpec, ArgSpec argSpec, Map info) { + if (!args.isEmpty()) { + final String arg = args.peek(); + if (commandSpec.parent().subcommands().get(arg) != null) { + final HelpCommand help = (HelpCommand) commandSpec.userObject(); + help.commands = arg; + args.pop(); + return true; + } + } + return false; + } + } + @Option(names = {"-h", "--help"}, usageHelp = true, descriptionKey = "helpCommand.help", description = "Show usage help for the help command and exit.") private boolean helpRequested; diff --git a/src/test/java/picocli/Issue2123RepeatableVsHelpTest.java b/src/test/java/picocli/Issue2123RepeatableVsHelpTest.java new file mode 100644 index 000000000..f18cd04a1 --- /dev/null +++ b/src/test/java/picocli/Issue2123RepeatableVsHelpTest.java @@ -0,0 +1,79 @@ +package picocli; + +import org.junit.Test; +import picocli.CommandLine.Command; +import picocli.CommandLine.HelpCommand; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.object.HasToString.hasToString; +import static picocli.CommandLine.ExitCode.OK; + +public class Issue2123RepeatableVsHelpTest { + + @Command(name = "tool", mixinStandardHelpOptions = true, subcommandsRepeatable = true) + static class Tool { + } + + @Command(name = "sub", mixinStandardHelpOptions = true) + static class Subcommand implements Runnable { + @Override + public void run() { + } + } + + private final StringWriter out = new StringWriter(); + + private final CommandLine commandLine = new CommandLine(new Tool()) + .addSubcommand(new Subcommand()) + .addSubcommand(new HelpCommand()) + .setOut(new PrintWriter(out)) + .setErr(new PrintWriter(out)); + + @Test + public void testToolHelpShowsToolUsage() { + final int result = commandLine.execute("help"); + + assertThat(out, hasToString(containsString("Usage: tool [-hV]"))); + assertThat(result, equalTo(OK)); + } + + @Test + public void testToolHelpSubShowsToolSubUsage() { + final int result = commandLine.execute("help", "sub"); + + assertThat(out, hasToString(containsString("Usage: tool sub"))); + assertThat(result, equalTo(OK)); + } + + @Test + public void testToolDashHSubShowsToolSubUsage() { + final int result = commandLine.execute("-h", "sub"); + + assertThat(out, hasToString(containsString("Usage: tool [-hV]"))); + assertThat(result, equalTo(OK)); + } + + @Test + public void testToolSubHelpShowsToolSubUsage() { + final int result = commandLine.execute("sub", "help"); + + // With subcommandsRepeatable = false, "help" wasn't allowed so returns USAGE showing usage for "sub" + // With subcommandsRepeatable = true, "help" is processed alone so returns OK showing usage for "tool" + // It would be helpful to note that "sub" is the latest command in use so return OK showing usage for "sub" + assertThat(out, hasToString(containsString("Usage: tool [-hV]"))); + assertThat(result, equalTo(OK)); + } + + @Test + public void testToolSubDashHShowsToolSubUsage() { + final int result = commandLine.execute("sub", "-h"); + + assertThat(out, hasToString(containsString("Usage: tool sub"))); + assertThat(result, equalTo(OK)); + } +}