Skip to content

Commit

Permalink
#139 Improve validation: CommandLine must be constructed with a comma…
Browse files Browse the repository at this point in the history
…nd that has at least one of @command, @option or @parameters annotation
  • Loading branch information
remkop committed Jun 4, 2017
1 parent 46ce37e commit 0628371
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 14 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
30 changes: 22 additions & 8 deletions src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -268,6 +269,7 @@ public List<String> getUnmatchedArguments() {
* @param args the command line arguments to parse
* @param <T> 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> T populateCommand(T command, String... args) {
Expand Down Expand Up @@ -295,6 +297,7 @@ public List<CommandLine> 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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -399,6 +404,7 @@ public void usage(PrintStream out, Help.ColorScheme colorScheme) {
* @param args the command line arguments to parse
* @param <R> 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 <R extends Runnable> void run(R command, PrintStream out, String... args) {
run(command, out, AUTO, args);
Expand All @@ -407,33 +413,34 @@ public static <R extends Runnable> 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:
* <pre>
* 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();
* </pre>
* Note that this method is not suitable for commands with subcommands.
* @param command the command to run when {@linkplain #populateCommand(Object, String...) parsing} succeeds.
* @param out the printStream to print to
* @param ansi whether the usage message should include ANSI escape codes or not
* @param args the command line arguments to parse
* @param <R> 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 <R extends Runnable> 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();
}

/**
Expand Down Expand Up @@ -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;
}
Expand All @@ -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");
}
}

/**
Expand Down
50 changes: 44 additions & 6 deletions src/test/java/picocli/CommandLineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -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));

Expand All @@ -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));
Expand All @@ -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;
}
Expand Down Expand Up @@ -2616,6 +2616,44 @@ public void run() {
" -number=<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 {
Expand Down

0 comments on commit 0628371

Please sign in to comment.