diff --git a/RELEASE-NOTES.adoc b/RELEASE-NOTES.adoc index 0f4ae39cd..c29016c9c 100644 --- a/RELEASE-NOTES.adoc +++ b/RELEASE-NOTES.adoc @@ -15,6 +15,7 @@ * #138 Improve validation: disallow option overwriting by default * #129 Make "allow option overwriting" configurable * #140 Make "allow unmatched arguments" configurable +* #139 Improve validation: CommandLine must be constructed with a command that has at least one of @Command, @Option or @Parameters annotation == 0.9.6 - Bugfix release for public review. API may change. diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index b1c5c3089..d4843553e 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -132,6 +132,7 @@ public class CommandLine { * When the {@link #parse(String...)} method is called, fields of the specified object that are annotated * with {@code @Option} or {@code @Parameters} will be initialized based on command line arguments. * @param command the object to initialize from the command line arguments + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation */ public CommandLine(Object command) { interpreter = new Interpreter(command); @@ -268,6 +269,7 @@ public List getUnmatchedArguments() { * @param args the command line arguments to parse * @param the type of the annotated object * @return the specified annotated object + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation * @throws ParameterException if the specified command line arguments are invalid */ public static T populateCommand(T command, String... args) { @@ -295,6 +297,7 @@ public List parse(String... args) { * Equivalent to {@code new CommandLine(command).usage(out)}. See {@link #usage(PrintStream)} for details. * @param command the object annotated with {@link Command}, {@link Option} and {@link Parameters} * @param out the print stream to print the help message to + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation */ public static void usage(Object command, PrintStream out) { toCommandLine(command).usage(out); @@ -306,6 +309,7 @@ public static void usage(Object command, PrintStream out) { * @param command the object annotated with {@link Command}, {@link Option} and {@link Parameters} * @param out the print stream to print the help message to * @param ansi whether the usage message should contain ANSI escape codes or not + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation */ public static void usage(Object command, PrintStream out, Help.Ansi ansi) { toCommandLine(command).usage(out, ansi); @@ -317,6 +321,7 @@ public static void usage(Object command, PrintStream out, Help.Ansi ansi) { * @param command the object annotated with {@link Command}, {@link Option} and {@link Parameters} * @param out the print stream to print the help message to * @param colorScheme the {@code ColorScheme} defining the styles for options, parameters and commands when ANSI is enabled + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation */ public static void usage(Object command, PrintStream out, Help.ColorScheme colorScheme) { toCommandLine(command).usage(out, colorScheme); @@ -399,6 +404,7 @@ public void usage(PrintStream out, Help.ColorScheme colorScheme) { * @param args the command line arguments to parse * @param the annotated object must implement Runnable * @see #run(Runnable, PrintStream, Help.Ansi, String...) + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation */ public static void run(R command, PrintStream out, String... args) { run(command, out, AUTO, args); @@ -407,15 +413,15 @@ public static void run(R command, PrintStream out, String.. * Convenience method to allow command line application authors to avoid some boilerplate code in their application. * The annotated object needs to implement {@link Runnable}. Calling this method is equivalent to: *
-     * Runnable runnable = null;
+     * CommandLine cmd = new CommandLine(command);
      * try {
-     *     runnable = populateCommand(command, args);
+     *     cmd.parse(args);
      * } catch (Exception ex) {
      *     System.err.println(ex.getMessage());
-     *     usage(command, out, ansi);
+     *     cmd.usage(out, ansi);
      *     return;
      * }
-     * runnable.run();
+     * command.run();
      * 
* Note that this method is not suitable for commands with subcommands. * @param command the command to run when {@linkplain #populateCommand(Object, String...) parsing} succeeds. @@ -423,17 +429,18 @@ public static void run(R command, PrintStream out, String.. * @param ansi whether the usage message should include ANSI escape codes or not * @param args the command line arguments to parse * @param the annotated object must implement Runnable + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation */ public static void run(R command, PrintStream out, Help.Ansi ansi, String... args) { - Runnable runnable = null; + CommandLine cmd = new CommandLine(command); // validate command outside of try-catch try { - runnable = populateCommand(command, args); + cmd.parse(args); } catch (Exception ex) { out.println(ex.getMessage()); - usage(command, out, ansi); + cmd.usage(out, ansi); return; } - runnable.run(); + command.run(); } /** @@ -1195,9 +1202,11 @@ private class Interpreter { this.command = Assert.notNull(command, "command"); Class cls = command.getClass(); String declaredSeparator = null; + boolean hasCommandAnnotation = false; while (cls != null) { init(cls, requiredFields, optionName2Field, singleCharOption2Field, positionalParametersFields); if (cls.isAnnotationPresent(Command.class)) { + hasCommandAnnotation = true; Command cmd = cls.getAnnotation(Command.class); declaredSeparator = (declaredSeparator == null) ? cmd.separator() : declaredSeparator; } @@ -1206,6 +1215,11 @@ private class Interpreter { separator = declaredSeparator != null ? declaredSeparator : separator; Collections.sort(positionalParametersFields, new PositionalParametersSorter()); validatePositionalParameters(positionalParametersFields); + + if (positionalParametersFields.isEmpty() && optionName2Field.isEmpty() && !hasCommandAnnotation) { + throw new IllegalArgumentException(command + " (" + command.getClass() + + ") is not a command: it has no @Command, @Option or @Parameters annotations"); + } } /** diff --git a/src/test/java/picocli/CommandLineTest.java b/src/test/java/picocli/CommandLineTest.java index c9c789570..cd53a86f7 100644 --- a/src/test/java/picocli/CommandLineTest.java +++ b/src/test/java/picocli/CommandLineTest.java @@ -2383,9 +2383,9 @@ public void testParseSubCommands() { @Test public void testCommandListReturnsRegisteredCommands() { - class MainCommand {} - class Command1 {} - class Command2 {} + @Command class MainCommand {} + @Command class Command1 {} + @Command class Command2 {} CommandLine commandLine = new CommandLine(new MainCommand()); commandLine.addSubcommand("cmd1", new Command1()).addSubcommand("cmd2", new Command2()); @@ -2559,7 +2559,7 @@ public void testCustomTypeConverterNotRegisteredAtAll() { @Test(expected = MissingTypeConverterException.class) public void testCustomTypeConverterRegisteredBeforeSubcommandsAdded() { - class TopLevel {} + @Command class TopLevel {} CommandLine commandLine = new CommandLine(new TopLevel()); commandLine.registerConverter(CustomType.class, new CustomType(null)); @@ -2569,7 +2569,7 @@ class TopLevel {} @Test public void testCustomTypeConverterRegisteredAfterSubcommandsAdded() { - class TopLevel { public boolean equals(Object o) {return getClass().equals(o.getClass());}} + @Command class TopLevel { public boolean equals(Object o) {return getClass().equals(o.getClass());}} CommandLine commandLine = new CommandLine(new TopLevel()); commandLine.addSubcommand("main", createNestedCommand()); commandLine.registerConverter(CustomType.class, new CustomType(null)); @@ -2584,7 +2584,7 @@ class TopLevel { public boolean equals(Object o) {return getClass().equals(o.get @Test public void testRunCallsRunnableIfParseSucceeds() { final boolean[] runWasCalled = {false}; - class App implements Runnable { + @Command class App implements Runnable { public void run() { runWasCalled[0] = true; } @@ -2616,6 +2616,44 @@ public void run() { " -number=%n"), result); } + @Test(expected = IllegalArgumentException.class) + public void testRunRequiresAnnotatedCommand() { + class App implements Runnable { + public void run() { } + } + CommandLine.run(new App(), System.err); + } + + @Test(expected = IllegalArgumentException.class) + public void testPopulateCommandRequiresAnnotatedCommand() { + class App { } + CommandLine.populateCommand(new App()); + } + + @Test(expected = IllegalArgumentException.class) + public void testUsageObjectPrintstreamRequiresAnnotatedCommand() { + class App { } + CommandLine.usage(new App(), System.out); + } + + @Test(expected = IllegalArgumentException.class) + public void testUsageObjectPrintstreamAnsiRequiresAnnotatedCommand() { + class App { } + CommandLine.usage(new App(), System.out, Help.Ansi.OFF); + } + + @Test(expected = IllegalArgumentException.class) + public void testUsageObjectPrintstreamColorschemeRequiresAnnotatedCommand() { + class App { } + CommandLine.usage(new App(), System.out, Help.defaultColorScheme(Help.Ansi.OFF)); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorRequiresAnnotatedCommand() { + class App { } + new CommandLine(new App()); + } + @Test public void testOverwrittenOptionDisallowedByDefault() { class App {