");
+ aBuilder.append("
");
+ aBuilder.append(programName).append(" ");
+ aBuilder.append(groupName).append(" ");
+ aBuilder.append(command.getName()).append(" ");
+ aBuilder.append("—");
+ aBuilder.append(htmlize(command.getDescription()));
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+
+ aBuilder.append(NEWLINE);
+ aBuilder.append(" options = newArrayList();
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+
+ if (programName != null) {
+ aBuilder.append(programName).append(" ").append(htmlize(Joiner.on(" ").join(toSynopsisUsage(sortOptions(command.getGlobalOptions())))));
+ options.addAll(command.getGlobalOptions());
+ aBuilder.append(" ");
+ }
+ if (groupName != null) {
+ aBuilder.append(groupName).append(" ").append(htmlize(Joiner.on(" ").join(toSynopsisUsage(sortOptions(command.getGroupOptions())))));
+ options.addAll(command.getGroupOptions());
+ aBuilder.append(" ");
+ }
+ aBuilder.append(command.getName()).append(" ").append(htmlize(Joiner.on(" ").join(toSynopsisUsage(sortOptions(command.getCommandOptions())))));
+ options.addAll(command.getCommandOptions());
+
+ // command arguments (optional)
+ ArgumentsMetadata arguments = command.getArguments();
+ if (arguments != null) {
+ aBuilder.append(" [--] ")
+ .append(htmlize(UsageHelper.toUsage(arguments)));
+ }
+
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+
+
+ //
+ // OPTIONS
+ //
+ if (options.size() > 0 || arguments != null) {
+ options = sortOptions(options);
+
+ aBuilder.append(NEWLINE);
+ aBuilder.append("OPTIONS \n").append(NEWLINE);
+
+ for (OptionMetadata option : options) {
+ // skip hidden options
+ if (option.isHidden()) {
+ continue;
+ }
+
+ // option names
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+ aBuilder.append(htmlize(UsageHelper.toDescription(option)));
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+
+ // description
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+ aBuilder.append(htmlize(option.getDescription()));
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+ }
+
+ if (arguments != null) {
+ // "--" option
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+
+ aBuilder.append("--\n");
+
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+
+ // description
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+
+ aBuilder.append("This option can be used to separate command-line options from the " +
+ "list of argument, (useful when arguments might be mistaken for command-line options\n");
+
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+
+ // arguments name
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+
+ aBuilder.append(htmlize(UsageHelper.toDescription(arguments)));
+
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+
+ // description
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+
+ aBuilder.append(htmlize(arguments.getDescription()));
+
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+ }
+ }
+
+ if (command.getDiscussion() != null) {
+ aBuilder.append(NEWLINE);
+ aBuilder.append("DISCUSSION \n").append(NEWLINE);
+
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+
+ aBuilder.append(htmlize(command.getDiscussion()));
+
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+ }
+
+ if (command.getExamples() != null && !command.getExamples().isEmpty()) {
+ aBuilder.append(NEWLINE);
+ aBuilder.append("EXAMPLES \n").append(NEWLINE);
+
+ aBuilder.append("\n");
+ aBuilder.append("
\n");
+
+ // this will only work for "well-formed" examples
+ for (int i = 0; i < command.getExamples().size(); i+=3) {
+ String aText = command.getExamples().get(i).trim();
+ String aEx = htmlize(command.getExamples().get(i+1));
+
+ if (aText.startsWith("*")) {
+ aText = aText.substring(1).trim();
+ }
+
+ aBuilder.append("
\n");
+ aBuilder.append(aText);
+ aBuilder.append("
\n");
+
+ aBuilder.append("
\n");
+ aBuilder.append(aEx);
+ aBuilder.append(" \n");
+ }
+
+ aBuilder.append("
\n");
+ aBuilder.append("
\n");
+ }
+
+ aBuilder.append("\n");
+ aBuilder.append("\n");
+
+ return aBuilder.toString();
+ }
+
+ public String usageMD(@Nullable String programName, @Nullable String groupName, CommandMetadata command) {
+
+ final StringBuilder aBuilder = new StringBuilder("");
+ final String br = " ";
+ final String np = " \n"; //new paragraph
+
+ aBuilder.append("# ").append(programName).append(" ").append(groupName).append(" ").append(command.getName()).append(" Manual Page\n");
+ aBuilder.append("## Description\n");
+ aBuilder.append(command.getDescription()).append(np);
+ aBuilder.append("## Usage\n`");
+ List options = newArrayList();
+ if (programName != null) {
+ aBuilder.append(programName).append(" ").append(Joiner.on(" ").join(toSynopsisUsage(sortOptions(command.getGlobalOptions()))));
+ options.addAll(command.getGlobalOptions());
+ aBuilder.append(" ");
+ }
+ if (groupName != null) {
+ aBuilder.append(groupName).append(" ").append(Joiner.on(" ").join(toSynopsisUsage(sortOptions(command.getGroupOptions()))));
+ options.addAll(command.getGroupOptions());
+ aBuilder.append(" ");
+ }
+ aBuilder.append(command.getName()).append(" ").append((Joiner.on(" ").join(toSynopsisUsage(sortOptions(command.getCommandOptions())))));
+ options.addAll(command.getCommandOptions());
+
+ ArgumentsMetadata arguments = command.getArguments();
+ if (arguments != null) {
+ aBuilder.append(" [--] ")
+ .append(UsageHelper.toUsage(arguments));
+ }
+ aBuilder.append("`\n");
+
+ if (options.size() > 0 || arguments != null) {
+ options = sortOptions(options);
+ aBuilder.append("## Options\n");
+ aBuilder.append("Name, shorthand | Description\n");
+ aBuilder.append("---|---\n");
+
+ for (OptionMetadata option : options) {
+ // skip hidden options
+ if (option.isHidden()) {
+ continue;
+ }
+ // option names
+ aBuilder.append("`");
+ aBuilder.append(UsageHelper.toDescription(option));
+ aBuilder.append("` | ");
+
+ // description
+ aBuilder.append(option.getDescription());
+ aBuilder.append("\n");
+ }
+
+ if (arguments != null) {
+ // "--" option
+ aBuilder.append("`--` | This option can be used to separate command-line options from the " +
+ "list of argument(s). (Useful when an argument might be mistaken for a command-line option)\n");
+
+ // arguments name
+ aBuilder.append("`").append(UsageHelper.toDescription(arguments)).append("` | ");
+
+ // description
+ aBuilder.append(arguments.getDescription()).append("\n");
+ }
+ }
+
+ if (command.getDiscussion() != null) {
+ aBuilder.append("## Discussion\n").append(command.getDiscussion()).append("\n");
+ }
+
+ if (command.getExamples() != null && !command.getExamples().isEmpty()) {
+ aBuilder.append("## Examples\n");
+
+ // this will only work for "well-formed" examples
+ for (int i = 0; i < command.getExamples().size(); i+=3) {
+ String aText = command.getExamples().get(i).trim();
+ String aEx = command.getExamples().get(i+1);
+
+ if (aText.startsWith("*")) {
+ aText = aText.substring(1).trim();
+ }
+ aBuilder.append(aText).append("\n```\n").append(aEx).append("\n```\n");
+ }
+ }
+
+ return aBuilder.toString();
+ }
+
+ private static final String htmlize(final String theStr) {
+ return theStr.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\n", " ");
+ }
}
diff --git a/src/main/java/io/airlift/command/GlobalUsage.java b/src/main/java/io/airlift/command/GlobalUsage.java
index 9410204fe..b7262ef04 100644
--- a/src/main/java/io/airlift/command/GlobalUsage.java
+++ b/src/main/java/io/airlift/command/GlobalUsage.java
@@ -76,7 +76,7 @@ public void usage(GlobalMetadata global, UsagePrinter out)
out.newIndentedPrinter(8).newPrinterWithHangingIndent(8)
.append(global.getName())
.appendWords(UsageHelper.toSynopsisUsage(global.getOptions()))
- .append(" []")
+ .append(" [ ]")
.newline()
.newline();
@@ -92,6 +92,12 @@ public void usage(GlobalMetadata global, UsagePrinter out)
out.append("OPTIONS").newline();
for (OptionMetadata option : options) {
+
+ if (option.isHidden())
+ {
+ continue;
+ }
+
// option names
UsagePrinter optionPrinter = out.newIndentedPrinter(8);
optionPrinter.append(UsageHelper.toDescription(option)).newline();
@@ -122,13 +128,16 @@ public void usage(GlobalMetadata global, UsagePrinter out)
private void printCommandDescription(UsagePrinter commandPrinter, @Nullable CommandGroupMetadata group, CommandMetadata command)
{
- if (group != null) {
- commandPrinter.append(group.getName());
- }
- commandPrinter.append(command.getName()).newline();
- if (command.getDescription() != null) {
- commandPrinter.newIndentedPrinter(4).append(command.getDescription()).newline();
+ if(!command.isHidden())
+ {
+ if (group != null) {
+ commandPrinter.append(group.getName());
+ }
+ commandPrinter.append(command.getName()).newline();
+ if (command.getDescription() != null) {
+ commandPrinter.newIndentedPrinter(4).append(command.getDescription()).newline();
+ }
+ commandPrinter.newline();
}
- commandPrinter.newline();
}
}
diff --git a/src/main/java/io/airlift/command/GlobalUsageSummary.java b/src/main/java/io/airlift/command/GlobalUsageSummary.java
index e009329e1..c35f36f97 100644
--- a/src/main/java/io/airlift/command/GlobalUsageSummary.java
+++ b/src/main/java/io/airlift/command/GlobalUsageSummary.java
@@ -1,7 +1,7 @@
package io.airlift.command;
import com.google.common.base.Function;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
@@ -11,6 +11,7 @@
import io.airlift.command.model.GlobalMetadata;
import io.airlift.command.model.OptionMetadata;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -60,21 +61,24 @@ public void usage(GlobalMetadata global, UsagePrinter out)
// build arguments
List commandArguments = newArrayList();
- commandArguments.addAll(Collections2.transform(global.getOptions(), new Function()
+ Collection args = Collections2.transform(global.getOptions(), new Function()
{
public String apply(OptionMetadata option)
{
- if (option.isHidden()) {
- return null;
+ if (option.isHidden())
+ {
+ return "";
}
return toUsage(option);
}
- }));
+ });
+
+ commandArguments.addAll(args);
out.newPrinterWithHangingIndent(8)
.append("usage:")
.append(global.getName())
.appendWords(commandArguments)
- .append(" []")
+ .append(" [ ]")
.newline()
.newline();
@@ -92,12 +96,12 @@ public String apply(OptionMetadata option)
commands.put(commandGroupMetadata.getName(), commandGroupMetadata.getDescription());
}
- out.append("The most commonly used ").append(global.getName()).append(" commands are:").newline();
+ out.append("Commands are:").newline();
out.newIndentedPrinter(4).appendTable(Iterables.transform(commands.entrySet(), new Function, Iterable>()
{
public Iterable apply(Entry entry)
{
- return ImmutableList.of(entry.getKey(), Objects.firstNonNull(entry.getValue(), ""));
+ return ImmutableList.of(entry.getKey(), MoreObjects.firstNonNull(entry.getValue(), ""));
}
}));
out.newline();
diff --git a/src/main/java/io/airlift/command/Group.java b/src/main/java/io/airlift/command/Group.java
new file mode 100644
index 000000000..b52fd3ee4
--- /dev/null
+++ b/src/main/java/io/airlift/command/Group.java
@@ -0,0 +1,41 @@
+package io.airlift.command;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Marks a class as providing command group metadata
+ */
+@Target(TYPE)
+@Retention(RUNTIME)
+@Inherited
+@Documented
+public @interface Group
+{
+ public static final class DEFAULT {}
+
+ /**
+ * Name of the group.
+ */
+ String name();
+
+ /**
+ * Description of the group.
+ */
+ String description() default "";
+
+ /**
+ * Default command class for the group (optional)
+ */
+ Class> defaultCommand() default DEFAULT.class;
+
+ /**
+ * command classes to add to the group (optional)
+ */
+ Class>[] commands() default {};
+}
diff --git a/src/main/java/io/airlift/command/Groups.java b/src/main/java/io/airlift/command/Groups.java
new file mode 100644
index 000000000..a7c192dce
--- /dev/null
+++ b/src/main/java/io/airlift/command/Groups.java
@@ -0,0 +1,21 @@
+package io.airlift.command;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Marks a class as providing multiple command group metadata
+ */
+@Target(TYPE)
+@Retention(RUNTIME)
+@Inherited
+@Documented
+public @interface Groups
+{
+ Group[] value();
+}
diff --git a/src/main/java/io/airlift/command/Help.java b/src/main/java/io/airlift/command/Help.java
index bcca42b8a..d32afc396 100644
--- a/src/main/java/io/airlift/command/Help.java
+++ b/src/main/java/io/airlift/command/Help.java
@@ -1,9 +1,14 @@
+// Copyright (c) 2010 - 2013, Clark & Parsia, LLC.
+// For more information about licensing and copyright of this software, please contact
+// inquiries@clarkparsia.com or visit http://stardog.com
+
package io.airlift.command;
import io.airlift.command.model.CommandGroupMetadata;
import io.airlift.command.model.CommandMetadata;
import io.airlift.command.model.GlobalMetadata;
+import javax.annotation.Nullable;
import javax.inject.Inject;
import java.util.List;
import java.util.concurrent.Callable;
@@ -11,47 +16,51 @@
import static com.google.common.collect.Lists.newArrayList;
@Command(name = "help", description = "Display help information")
-public class Help implements Runnable, Callable
-{
+public class Help implements Runnable, Callable {
+ public static boolean USAGE_AS_HTML = false;
+ public static boolean USAGE_AS_RONN = false;
+ public static boolean USAGE_AS_MD = false;
+
@Inject
+ @Nullable
public GlobalMetadata global;
@Arguments
public List command = newArrayList();
@Override
- public void run()
+ public void run() throws UnsupportedOperationException
{
help(global, command);
}
@Override
- public Void call()
+ public Void call() throws UnsupportedOperationException
{
run();
return null;
}
- public static void help(CommandMetadata command)
+ public static void help(CommandMetadata command) throws UnsupportedOperationException
{
StringBuilder stringBuilder = new StringBuilder();
help(command, stringBuilder);
System.out.println(stringBuilder.toString());
}
- public static void help(CommandMetadata command, StringBuilder out)
+ public static void help(CommandMetadata command, StringBuilder out) throws UnsupportedOperationException
{
new CommandUsage().usage(null, null, command.getName(), command, out);
}
- public static void help(GlobalMetadata global, List commandNames)
+ public static void help(GlobalMetadata global, List commandNames) throws UnsupportedOperationException
{
StringBuilder stringBuilder = new StringBuilder();
help(global, commandNames, stringBuilder);
System.out.println(stringBuilder.toString());
}
- public static void help(GlobalMetadata global, List commandNames, StringBuilder out)
+ public static void help(GlobalMetadata global, List commandNames, StringBuilder out) throws UnsupportedOperationException
{
if (commandNames.isEmpty()) {
new GlobalUsageSummary().usage(global, out);
@@ -69,7 +78,18 @@ public static void help(GlobalMetadata global, List commandNames, String
// command in the default group?
for (CommandMetadata command : global.getDefaultGroupCommands()) {
if (name.equals(command.getName())) {
- new CommandUsage().usage(global.getName(), null, command.getName(), command, out);
+ if (USAGE_AS_HTML) {
+ out.append(new CommandUsage().usageHTML(global.getName(), null, command));
+ }
+ else if (USAGE_AS_RONN) {
+ out.append(new CommandUsage().usageRonn(global.getName(), null, command));
+ }
+ else if (USAGE_AS_MD) {
+ out.append(new CommandUsage().usageMD(global.getName(), null, command));
+ }
+ else {
+ new CommandUsage().usage(global.getName(), null, command.getName(), command, out);
+ }
return;
}
}
@@ -86,15 +106,27 @@ public static void help(GlobalMetadata global, List commandNames, String
String commandName = commandNames.get(1);
for (CommandMetadata command : group.getCommands()) {
if (commandName.equals(command.getName())) {
- new CommandUsage().usage(global.getName(), group.getName(), command.getName(), command, out);
+ if (USAGE_AS_HTML) {
+ out.append(new CommandUsage().usageHTML(global.getName(), group.getName(), command));
+ }
+ else if (USAGE_AS_RONN) {
+ out.append(new CommandUsage().usageRonn(global.getName(), group.getName(), command));
+ }
+ else if (USAGE_AS_MD) {
+ out.append(new CommandUsage().usageMD(global.getName(), group.getName(), command));
+ }
+ else {
+ new CommandUsage().usage(global.getName(), group.getName(), command.getName(), command, out);
+ }
+
return;
}
}
- System.out.println("Unknown command " + name + " " + commandName);
+ throw new UnsupportedOperationException("Unknown command " + name + " " + commandName);
}
}
}
- System.out.println("Unknown command " + name);
+ throw new UnsupportedOperationException("Unknown command " + name);
}
}
diff --git a/src/main/java/io/airlift/command/ParseArgumentsMissingException.java b/src/main/java/io/airlift/command/ParseArgumentsMissingException.java
index 3ef1133eb..f9fceb509 100644
--- a/src/main/java/io/airlift/command/ParseArgumentsMissingException.java
+++ b/src/main/java/io/airlift/command/ParseArgumentsMissingException.java
@@ -18,22 +18,23 @@
package io.airlift.command;
-import com.google.common.collect.ImmutableList;
-
import java.util.List;
+import com.google.common.base.Joiner;
+
+
public class ParseArgumentsMissingException extends ParseException
{
- private final String argumentTitle;
+ private final List argumentTitles;
- ParseArgumentsMissingException(String argumentTitle)
+ ParseArgumentsMissingException(List argumentTitles)
{
- super("Required parameters are missing: %s", argumentTitle);
- this.argumentTitle = argumentTitle;
+ super("Required arguments are missing: '%s'", Joiner.on(',').join(argumentTitles));
+ this.argumentTitles = argumentTitles;
}
- public String getArgumentTitle()
+ public List getArgumentTitle()
{
- return argumentTitle;
+ return argumentTitles;
}
}
diff --git a/src/main/java/io/airlift/command/ParseOptionIllegalValueException.java b/src/main/java/io/airlift/command/ParseOptionIllegalValueException.java
new file mode 100644
index 000000000..b53dc321d
--- /dev/null
+++ b/src/main/java/io/airlift/command/ParseOptionIllegalValueException.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (C) 2010 the original author or authors.
+ * See the notice.md file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.airlift.command;
+
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+
+public class ParseOptionIllegalValueException extends ParseException
+{
+ private final String optionTitle, illegalValue;
+ private final Set allowedValues;
+
+ ParseOptionIllegalValueException(String optionTitle, String value, Set allowedValues)
+ {
+ super("Value for option '%s' was given as '%s' which is not in the list of allowed values: %s", optionTitle, value, allowedValues);
+ this.optionTitle = optionTitle;
+ this.illegalValue = value;
+ this.allowedValues = ImmutableSet.copyOf(allowedValues);
+ }
+
+ public String getOptionTitle()
+ {
+ return optionTitle;
+ }
+
+ public String getIllegalValue()
+ {
+ return illegalValue;
+ }
+
+ public Set getAllowedValues()
+ {
+ return allowedValues;
+ }
+}
diff --git a/src/main/java/io/airlift/command/Parser.java b/src/main/java/io/airlift/command/Parser.java
index 05ab20f38..b450be6f4 100644
--- a/src/main/java/io/airlift/command/Parser.java
+++ b/src/main/java/io/airlift/command/Parser.java
@@ -54,19 +54,29 @@ public ParseState parse(GlobalMetadata metadata, Iterable params)
}
if (tokens.hasNext()) {
- CommandMetadata command = find(expectedCommands, compose(equalTo(tokens.peek()), CommandMetadata.nameGetter()), null);
+ CommandMetadata command = find(expectedCommands,
+ compose(equalTo(tokens.peek()), CommandMetadata.nameGetter()),
+ state.getGroup() != null ? state.getGroup().getDefaultCommand() : null);
+
+ if (command == null && state.getGroup() == null && metadata.getDefaultCommand() != null) {
+ command = metadata.getDefaultCommand();
+ }
+
if (command == null) {
while (tokens.hasNext()) {
state = state.withUnparsedInput(tokens.next());
}
}
else {
- tokens.next();
+ if (tokens.peek().equals(command.getName())) {
+ tokens.next();
+ }
+
state = state.withCommand(command).pushContext(Context.COMMAND);
while (tokens.hasNext()) {
state = parseOptions(tokens, state, command.getCommandOptions());
-
+
state = parseArgs(state, tokens, command.getArguments());
}
}
@@ -139,7 +149,9 @@ private ParseState parseSimpleOption(PeekingIterator tokens, ParseState
}
else if (option.getArity() == 1) {
if (tokens.hasNext()) {
- value = TypeConverter.newInstance().convert(option.getTitle(), option.getJavaType(), tokens.next());
+ String tokenStr = tokens.next();
+ checkValidValue(option, tokenStr);
+ value = TypeConverter.newInstance().convert(option.getTitle(), option.getJavaType(), tokenStr);
state = state.withOptionValue(option, value).popContext();
}
}
@@ -147,12 +159,22 @@ else if (option.getArity() == 1) {
ImmutableList.Builder values = ImmutableList.builder();
int count = 0;
- while (count < option.getArity() && tokens.hasNext()) {
- values.add(TypeConverter.newInstance().convert(option.getTitle(), option.getJavaType(), tokens.next()));
+
+ boolean hasSeparator = false;
+ boolean foundNextOption = false;
+ while (count < option.getArity() && tokens.hasNext() && !hasSeparator) {
+ String peekedToken = tokens.peek();
+ hasSeparator = peekedToken.equals("--");
+ foundNextOption = findOption(allowedOptions, peekedToken) != null;
+
+ if (hasSeparator || foundNextOption) break;
+ String tokenStr = tokens.next();
+ checkValidValue(option, tokenStr);
+ values.add(TypeConverter.newInstance().convert(option.getTitle(), option.getJavaType(), tokenStr));
++count;
}
- if (count == option.getArity()) {
+ if (count == option.getArity() || hasSeparator || foundNextOption) {
state = state.withOptionValue(option, values.build()).popContext();
}
}
@@ -177,6 +199,7 @@ private ParseState parseLongGnuGetOpt(PeekingIterator tokens, ParseState
// update state
state = state.pushContext(Context.OPTION).withOption(option);
+ checkValidValue(option, parts.get(1));
Object value = TypeConverter.newInstance().convert(option.getTitle(), option.getJavaType(), parts.get(1));
state = state.withOption(option).withOptionValue(option, value).popContext();
@@ -219,11 +242,14 @@ private ParseState parseClassicGetOpt(PeekingIterator tokens, ParseState
// if current token has more characters, this is the value; otherwise it is the next token
if (!remainingToken.isEmpty()) {
+ checkValidValue(option, remainingToken);
Object value = TypeConverter.newInstance().convert(option.getTitle(), option.getJavaType(), remainingToken);
nextState = nextState.withOptionValue(option, value).popContext();
}
else if (tokens.hasNext()) {
- Object value = TypeConverter.newInstance().convert(option.getTitle(), option.getJavaType(), tokens.next());
+ String tokenStr = tokens.next();
+ checkValidValue(option, tokenStr);
+ Object value = TypeConverter.newInstance().convert(option.getTitle(), option.getJavaType(), tokenStr);
nextState = nextState.withOptionValue(option, value).popContext();
}
@@ -238,6 +264,17 @@ else if (tokens.hasNext()) {
return nextState;
}
+
+ /**
+ * Checks for a valid value and throws an error if the value for the option is restricted and not in the set of allowed values
+ * @param option Option meta data
+ * @param tokenStr Token string
+ */
+ private void checkValidValue(OptionMetadata option, String tokenStr) {
+ if (option.getAllowedValues() == null) return;
+ if (option.getAllowedValues().contains(tokenStr)) return;
+ throw new ParseOptionIllegalValueException(option.getTitle(), tokenStr, option.getAllowedValues());
+ }
private ParseState parseArgs(ParseState state, PeekingIterator tokens, ArgumentsMetadata arguments)
{
@@ -262,7 +299,9 @@ private ParseState parseArgs(ParseState state, PeekingIterator tokens, A
private ParseState parseArg(ParseState state, PeekingIterator tokens, ArgumentsMetadata arguments)
{
if (arguments != null) {
- state = state.withArgument(TypeConverter.newInstance().convert(arguments.getTitle(), arguments.getJavaType(), tokens.next()));
+ // TODO: check each title one by one? see: https://github.com/airlift/airline/issues/6
+ state = state.withArgument(TypeConverter.newInstance()
+ .convert(arguments.getTitle().get(0), arguments.getJavaType(), tokens.next()));
}
else {
state = state.withUnparsedInput(tokens.next());
diff --git a/src/main/java/io/airlift/command/ParserUtil.java b/src/main/java/io/airlift/command/ParserUtil.java
index ad27486ad..87f7f6ce7 100644
--- a/src/main/java/io/airlift/command/ParserUtil.java
+++ b/src/main/java/io/airlift/command/ParserUtil.java
@@ -26,16 +26,25 @@ public static T createInstance(Class type)
}
public static T createInstance(Class> type,
- Iterable options,
- ListMultimap parsedOptions,
- ArgumentsMetadata arguments,
- Iterable parsedArguments,
- Iterable metadataInjection,
- Map, Object> bindings)
+ Iterable options,
+ ListMultimap parsedOptions,
+ ArgumentsMetadata arguments,
+ Iterable parsedArguments,
+ Iterable metadataInjection,
+ Map, Object> bindings)
{
- // create the command instance
- T commandInstance = (T) ParserUtil.createInstance(type);
-
+ return createInstance(type, options, parsedOptions, arguments, parsedArguments, metadataInjection, bindings, new CommandFactoryDefault());
+ }
+
+
+ public static T injectOptions(T commandInstance,
+ Iterable options,
+ ListMultimap parsedOptions,
+ ArgumentsMetadata arguments,
+ Iterable parsedArguments,
+ Iterable metadataInjection,
+ Map, Object> bindings)
+ {
// inject options
for (OptionMetadata option : options) {
List> values = parsedOptions.get(option);
@@ -49,23 +58,38 @@ public static T createInstance(Class> type,
}
}
}
-
+
// inject args
if (arguments != null && parsedArguments != null) {
for (Accessor accessor : arguments.getAccessors()) {
accessor.addValues(commandInstance, parsedArguments);
}
}
-
+
for (Accessor accessor : metadataInjection) {
Object injectee = bindings.get(accessor.getJavaType());
-
+
if (injectee != null) {
accessor.addValues(commandInstance, ImmutableList.of(injectee));
}
}
-
+
return commandInstance;
}
+
+
+ public static T createInstance(Class> type,
+ Iterable options,
+ ListMultimap parsedOptions,
+ ArgumentsMetadata arguments,
+ Iterable parsedArguments,
+ Iterable metadataInjection,
+ Map, Object> bindings,
+ CommandFactory commandFactory)
+ {
+ // create the command instance
+ T commandInstance = (T) commandFactory.createInstance(type);
+ return injectOptions(commandInstance, options, parsedOptions, arguments, parsedArguments, metadataInjection, bindings);
+ }
}
diff --git a/src/main/java/io/airlift/command/UsageHelper.java b/src/main/java/io/airlift/command/UsageHelper.java
index ea3cfea9e..b02732728 100644
--- a/src/main/java/io/airlift/command/UsageHelper.java
+++ b/src/main/java/io/airlift/command/UsageHelper.java
@@ -2,6 +2,7 @@
import com.google.common.base.Function;
import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
@@ -83,13 +84,55 @@ public String apply(@Nullable String option)
return stringBuilder.toString();
}
+ public static String toRonnDescription(OptionMetadata option)
+ {
+ Set options = option.getOptions();
+ StringBuilder stringBuilder = new StringBuilder();
+
+ final String argumentString;
+ if (option.getArity() > 0) {
+ argumentString = Joiner.on(" ").join(Lists.transform(ImmutableList.of(option.getTitle()), new Function()
+ {
+ public String apply(@Nullable String argument)
+ {
+ return "<" + argument + ">";
+ }
+ }));
+ } else {
+ argumentString = null;
+ }
+
+ Joiner.on(", ").appendTo(stringBuilder, transform(options, new Function()
+ {
+ public String apply(@Nullable String option)
+ {
+ if (argumentString != null) {
+ return "`" + option + "` " + argumentString;
+ }
+ return "`" + option + "`";
+ }
+ }));
+
+ return stringBuilder.toString();
+ }
+
public static String toDescription(ArgumentsMetadata arguments)
{
if (!arguments.getUsage().isEmpty()) {
return arguments.getUsage();
}
+ List descriptionTitles = arguments.getTitle();
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String title : descriptionTitles) {
+ if (stringBuilder.length() > 0) {
+ stringBuilder.append(" ");
+ }
+ stringBuilder.append("<");
+ stringBuilder.append(title);
+ stringBuilder.append(">");
+ }
- return "<" + arguments.getTitle() + ">";
+ return stringBuilder.toString();
}
@@ -99,11 +142,11 @@ public static String toUsage(OptionMetadata option)
boolean required = option.isRequired();
StringBuilder stringBuilder = new StringBuilder();
if (!required) {
- stringBuilder.append('[');
+ stringBuilder.append("[ ");
}
if (options.size() > 1) {
- stringBuilder.append('(');
+ stringBuilder.append('{');
}
final String argumentString;
@@ -124,17 +167,21 @@ public String apply(@Nullable String argument)
{
public String apply(@Nullable String option)
{
- if (argumentString != null) {
- return option + " " + argumentString;
- }
- else {
+// if (argumentString != null) {
+// return option + " " + argumentString;
+// }
+// else {
return option;
- }
+// }
}
}));
if (options.size() > 1) {
- stringBuilder.append(')');
+ stringBuilder.append('}');
+ }
+
+ if (argumentString != null) {
+ stringBuilder.append(" " + argumentString);
}
if (option.isMultiValued()) {
@@ -142,7 +189,7 @@ public String apply(@Nullable String option)
}
if (!required) {
- stringBuilder.append(']');
+ stringBuilder.append(" ]");
}
return stringBuilder.toString();
}
@@ -156,17 +203,18 @@ public static String toUsage(ArgumentsMetadata arguments)
boolean required = arguments.isRequired();
StringBuilder stringBuilder = new StringBuilder();
if (!required) {
- stringBuilder.append('[');
+ // TODO: be able to handle required arguments individually, like arity for the options
+ stringBuilder.append("[ ");
}
-
- stringBuilder.append("<").append(arguments.getTitle()).append(">");
+
+ stringBuilder.append(toDescription(arguments));
if (arguments.isMultiValued()) {
stringBuilder.append("...");
}
if (!required) {
- stringBuilder.append(']');
+ stringBuilder.append(" ]");
}
return stringBuilder.toString();
}
@@ -177,8 +225,26 @@ public static List toSynopsisUsage(List options)
{
public String apply(OptionMetadata option)
{
+ if (option.isHidden())
+ {
+ return "";
+ }
+
return toUsage(option);
}
}));
}
+
+ public static String toDefaultCommand(String command)
+ {
+ if (Strings.isNullOrEmpty(command)) {
+ return "";
+ }
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("[ ");
+ stringBuilder.append(command);
+ stringBuilder.append(" ]");
+
+ return stringBuilder.toString();
+ }
}
diff --git a/src/main/java/io/airlift/command/UsagePrinter.java b/src/main/java/io/airlift/command/UsagePrinter.java
index a31eb93ec..3eb53c096 100644
--- a/src/main/java/io/airlift/command/UsagePrinter.java
+++ b/src/main/java/io/airlift/command/UsagePrinter.java
@@ -1,6 +1,7 @@
package io.airlift.command;
import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -80,29 +81,59 @@ public UsagePrinter appendTable(Iterable extends Iterable> table)
line.append(" ");
column++;
}
- out.append(spaces(indent)).append(line.toString().trim()).append("\n");
+ out.append(spaces(indent)).append(trimEnd(line.toString())).append("\n");
}
return this;
}
- public UsagePrinter append(String value)
+ public static String trimEnd(final String str) {
+ if (Strings.isNullOrEmpty(str)) {
+ return str;
+ }
+
+ int end = str.length();
+ while ((end != 0) && Character.isWhitespace(str.charAt(end - 1))) {
+ end--;
+ }
+
+ return str.substring(0, end);
+ }
+
+ public UsagePrinter append(String value) {
+ return append(value, false);
+ }
+
+ public UsagePrinter appendOnOneLine(String value) {
+ return append(value, true);
+ }
+
+ public UsagePrinter appendWords(Iterable words) {
+ return appendWords(words, false);
+ }
+
+ public UsagePrinter append(String value, boolean avoidNewlines)
{
if (value == null) {
return this;
}
- return appendWords(Splitter.onPattern("\\s+").omitEmptyStrings().trimResults().split(String.valueOf(value)));
+ return appendWords(Splitter.onPattern("\\s+").omitEmptyStrings().trimResults().split(String.valueOf(value)), avoidNewlines);
}
- public UsagePrinter appendWords(Iterable words)
+ public UsagePrinter appendWords(Iterable words, boolean avoidNewlines)
{
+ int bracketCount = 0;
for (String word : words) {
+ if(null == word || "".equals(word))
+ {
+ continue;
+ }
if (currentPosition.get() == 0) {
// beginning of line
out.append(spaces(indent));
currentPosition.getAndAdd((indent));
}
- else if (word.length() > maxSize || currentPosition.get() + word.length() <= maxSize) {
+ else if (word.length() > maxSize || currentPosition.get() + word.length() <= maxSize || bracketCount > 0 || avoidNewlines) {
// between words
out.append(" ");
currentPosition.getAndIncrement();
@@ -115,6 +146,12 @@ else if (word.length() > maxSize || currentPosition.get() + word.length() <= max
out.append(word);
currentPosition.getAndAdd((word.length()));
+ if (word.contains("{") || word.contains("[") || word.contains("<")) {
+ bracketCount++;
+ }
+ if (word.contains("}") || word.contains("]") || word.contains(">")) {
+ bracketCount--;
+ }
}
return this;
}
diff --git a/src/main/java/io/airlift/command/model/ArgumentsMetadata.java b/src/main/java/io/airlift/command/model/ArgumentsMetadata.java
index 0cb94ba25..0621098b4 100644
--- a/src/main/java/io/airlift/command/model/ArgumentsMetadata.java
+++ b/src/main/java/io/airlift/command/model/ArgumentsMetadata.java
@@ -1,30 +1,33 @@
package io.airlift.command.model;
+import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import io.airlift.command.Accessor;
import java.lang.reflect.Field;
+import java.util.List;
import java.util.Set;
import static com.google.common.collect.Sets.newHashSet;
public class ArgumentsMetadata
{
- private final String title;
+ private final List titles;
private final String description;
private final String usage;
private final boolean required;
private final Set accessors;
- public ArgumentsMetadata(String title, String description, String usage, boolean required, Iterable path)
+ public ArgumentsMetadata(Iterable titles, String description, String usage, boolean required, Iterable path)
{
- Preconditions.checkNotNull(title, "title is null");
+ Preconditions.checkNotNull(titles, "title is null");
Preconditions.checkNotNull(path, "path is null");
Preconditions.checkArgument(!Iterables.isEmpty(path), "path is empty");
- this.title = title;
+ this.titles = ImmutableList.copyOf(titles);
this.description = description;
this.usage = usage;
this.required = required;
@@ -38,7 +41,7 @@ public ArgumentsMetadata(Iterable arguments)
ArgumentsMetadata first = arguments.iterator().next();
- this.title = first.title;
+ this.titles = first.titles;
this.description = first.description;
this.usage = first.usage;
this.required = first.required;
@@ -53,9 +56,9 @@ public ArgumentsMetadata(Iterable arguments)
this.accessors = ImmutableSet.copyOf(accessors);
}
- public String getTitle()
+ public List getTitle()
{
- return title;
+ return titles;
}
public String getDescription()
@@ -106,7 +109,7 @@ public boolean equals(Object o)
if (description != null ? !description.equals(that.description) : that.description != null) {
return false;
}
- if (!title.equals(that.title)) {
+ if (!titles.equals(that.titles)) {
return false;
}
if (usage != null ? !usage.equals(that.usage) : that.usage != null) {
@@ -119,7 +122,7 @@ public boolean equals(Object o)
@Override
public int hashCode()
{
- int result = title.hashCode();
+ int result = titles.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (usage != null ? usage.hashCode() : 0);
result = 31 * result + (required ? 1 : 0);
@@ -131,7 +134,7 @@ public String toString()
{
final StringBuilder sb = new StringBuilder();
sb.append("ArgumentsMetadata");
- sb.append("{title='").append(title).append('\'');
+ sb.append("{title='").append(Joiner.on(',').join(titles)).append('\'');
sb.append(", description='").append(description).append('\'');
sb.append(", usage='").append(usage).append('\'');
sb.append(", required=").append(required);
diff --git a/src/main/java/io/airlift/command/model/CommandGroupMetadata.java b/src/main/java/io/airlift/command/model/CommandGroupMetadata.java
index 08d7792c1..42a8cc8b2 100644
--- a/src/main/java/io/airlift/command/model/CommandGroupMetadata.java
+++ b/src/main/java/io/airlift/command/model/CommandGroupMetadata.java
@@ -1,9 +1,10 @@
package io.airlift.command.model;
+import java.util.List;
+
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
-
-import java.util.List;
+import com.google.common.collect.Lists;
public class CommandGroupMetadata
{
@@ -19,7 +20,7 @@ public CommandGroupMetadata(String name, String description, Iterable getCommands()
{
- return commands;
+ return ImmutableList.copyOf(commands);
+ }
+
+ public void addCommand(CommandMetadata command)
+ {
+ if(!commands.contains(command))
+ {
+ commands.add(command);
+ }
}
@Override
diff --git a/src/main/java/io/airlift/command/model/CommandMetadata.java b/src/main/java/io/airlift/command/model/CommandMetadata.java
index e9f4f647b..4da4b3a90 100644
--- a/src/main/java/io/airlift/command/model/CommandMetadata.java
+++ b/src/main/java/io/airlift/command/model/CommandMetadata.java
@@ -3,8 +3,8 @@
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import io.airlift.command.Accessor;
+import io.airlift.command.Group;
-import javax.annotation.Nullable;
import java.util.List;
public class CommandMetadata
@@ -18,15 +18,24 @@ public class CommandMetadata
private final ArgumentsMetadata arguments;
private final List metadataInjections;
private final Class> type;
+ private final List groupNames;
+ private final List groups;
+
+ private final List examples;
+ private final String discussion;
public CommandMetadata(String name,
- String description,
- boolean hidden, Iterable globalOptions,
- Iterable groupOptions,
- Iterable commandOptions,
- ArgumentsMetadata arguments,
- Iterable metadataInjections,
- Class> type)
+ String description,
+ final String discussion,
+ final List examples,
+ boolean hidden, Iterable globalOptions,
+ Iterable groupOptions,
+ Iterable commandOptions,
+ ArgumentsMetadata arguments,
+ Iterable metadataInjections,
+ Class> type,
+ List groupNames,
+ List groups)
{
this.name = name;
this.description = description;
@@ -37,6 +46,12 @@ public CommandMetadata(String name,
this.arguments = arguments;
this.metadataInjections = ImmutableList.copyOf(metadataInjections);
this.type = type;
+
+ this.discussion = discussion;
+ this.examples = examples;
+
+ this.groupNames = groupNames;
+ this.groups = groups;
}
public String getName()
@@ -60,6 +75,14 @@ public List getAllOptions()
}
+ public List getExamples() {
+ return examples;
+ }
+
+ public String getDiscussion() {
+ return discussion;
+ }
+
public List getGlobalOptions()
{
return globalOptions;
@@ -90,6 +113,16 @@ public Class> getType()
return type;
}
+ public List getGroupNames()
+ {
+ return groupNames;
+ }
+
+ public List getGroups()
+ {
+ return groups;
+ }
+
@Override
public String toString()
{
@@ -97,6 +130,8 @@ public String toString()
sb.append("CommandMetadata");
sb.append("{name='").append(name).append('\'');
sb.append(", description='").append(description).append('\'');
+ sb.append(", discussion='").append(discussion).append('\'');
+ sb.append(", examples='").append(examples).append('\'');
sb.append(", globalOptions=").append(globalOptions);
sb.append(", groupOptions=").append(groupOptions);
sb.append(", commandOptions=").append(commandOptions);
@@ -117,4 +152,15 @@ public String apply(CommandMetadata input)
}
};
}
+
+ public static Function typeGetter()
+ {
+ return new Function()
+ {
+ public Class> apply(CommandMetadata input)
+ {
+ return input.getType();
+ }
+ };
+ }
}
diff --git a/src/main/java/io/airlift/command/model/MetadataLoader.java b/src/main/java/io/airlift/command/model/MetadataLoader.java
index 0279108c9..e75d75332 100644
--- a/src/main/java/io/airlift/command/model/MetadataLoader.java
+++ b/src/main/java/io/airlift/command/model/MetadataLoader.java
@@ -2,24 +2,42 @@
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
+
+import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Sets;
import io.airlift.command.Accessor;
import io.airlift.command.Arguments;
import io.airlift.command.Command;
+import io.airlift.command.Group;
+import io.airlift.command.Groups;
import io.airlift.command.Option;
import io.airlift.command.OptionType;
import io.airlift.command.Suggester;
import javax.annotation.Nullable;
import javax.inject.Inject;
+
+import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import static com.google.common.base.Predicates.compose;
+import static com.google.common.base.Predicates.equalTo;
+import static com.google.common.collect.Iterables.find;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
@@ -74,13 +92,30 @@ public CommandMetadata apply(Class> commandType)
public static CommandMetadata loadCommand(Class> commandType)
{
+ if (commandType == null) {
+ return null;
+ }
+
Command command = null;
+ List groups = Lists.newArrayList();
+
for (Class> cls = commandType; command == null && !Object.class.equals(cls); cls = cls.getSuperclass()) {
command = cls.getAnnotation(Command.class);
+
+ if(cls.isAnnotationPresent(Groups.class))
+ {
+ groups.addAll(Arrays.asList(cls.getAnnotation(Groups.class).value()));
+ }
+ if(cls.isAnnotationPresent(Group.class))
+ {
+ groups.add(cls.getAnnotation(Group.class));
+ }
}
Preconditions.checkArgument(command != null, "Command %s is not annotated with @Command", commandType.getName());
String name = command.name();
String description = command.description().isEmpty() ? null : command.description();
+ List groupNames = Arrays.asList(command.groupNames());
+
boolean hidden = command.hidden();
InjectionMetadata injectionMetadata = loadInjectionMetadata(commandType);
@@ -88,12 +123,16 @@ public static CommandMetadata loadCommand(Class> commandType)
CommandMetadata commandMetadata = new CommandMetadata(
name,
description,
+ command.discussion().isEmpty() ? null : command.discussion(),
+ command.examples().length == 0 ? null : Lists.newArrayList(command.examples()),
hidden, injectionMetadata.globalOptions,
injectionMetadata.groupOptions,
injectionMetadata.commandOptions,
Iterables.getFirst(injectionMetadata.arguments, null),
injectionMetadata.metadataInjections,
- commandType);
+ commandType,
+ groupNames,
+ groups);
return commandMetadata;
@@ -115,6 +154,11 @@ public static InjectionMetadata loadInjectionMetadata(Class> type)
public static void loadInjectionMetadata(Class> type, InjectionMetadata injectionMetadata, List fields)
{
+ if(type.isInterface())
+ {
+ return;
+ }
+
for (Class> cls = type; !Object.class.equals(cls); cls = cls.getSuperclass()) {
for (Field field : cls.getDeclaredFields()) {
field.setAccessible(true);
@@ -131,6 +175,28 @@ public static void loadInjectionMetadata(Class> type, InjectionMetadata inject
}
}
+ try {
+ @SuppressWarnings("unchecked")
+ Annotation aGuiceInject = field.getAnnotation((Class extends Annotation>)Class.forName("com.google.inject.Inject"));
+ if (aGuiceInject != null) {
+ if (field.getType().equals(GlobalMetadata.class) ||
+ field.getType().equals(CommandGroupMetadata.class) ||
+ field.getType().equals(CommandMetadata.class)) {
+ injectionMetadata.metadataInjections.add(new Accessor(path));
+ } else {
+ loadInjectionMetadata(field.getType(), injectionMetadata, path);
+ }
+ }
+ }
+ catch (ClassNotFoundException e) {
+ // this is ok, means Guice is not on the class path, so probably not being used
+ // and thus, ok that this did not work.
+ }
+ catch (ClassCastException e) {
+ // ignore this too, we're doing some funky cross your fingers type reflect stuff to play
+ // nicely with Guice
+ }
+
Option optionAnnotation = field.getAnnotation(Option.class);
if (optionAnnotation != null) {
OptionType optionType = optionAnnotation.type();
@@ -184,19 +250,20 @@ public static void loadInjectionMetadata(Class> type, InjectionMetadata inject
Arguments argumentsAnnotation = field.getAnnotation(Arguments.class);
if (field.isAnnotationPresent(Arguments.class)) {
- String title;
- if (!argumentsAnnotation.title().isEmpty()) {
- title = argumentsAnnotation.title();
+ ImmutableList.Builder titlesBuilder = ImmutableList.builder();
+
+ if (!(argumentsAnnotation.title().length == 1 && argumentsAnnotation.title()[0].equals(""))) {
+ titlesBuilder.add(argumentsAnnotation.title());
}
else {
- title = field.getName();
+ titlesBuilder.add(field.getName());
}
String description = argumentsAnnotation.description();
String usage = argumentsAnnotation.usage();
boolean required = argumentsAnnotation.required();
- injectionMetadata.arguments.add(new ArgumentsMetadata(title, description, usage, required, path));
+ injectionMetadata.arguments.add(new ArgumentsMetadata(titlesBuilder.build(), description, usage, required, path));
}
}
}
@@ -204,7 +271,8 @@ public static void loadInjectionMetadata(Class> type, InjectionMetadata inject
private static List mergeOptionSet(List options)
{
- ListMultimap metadataIndex = ArrayListMultimap.create();
+// ListMultimap metadataIndex = ArrayListMultimap.create();
+ Multimap metadataIndex = Multimaps.newMultimap(Maps.>newLinkedHashMap(), new Supplier>() { public List get() { return Lists.newArrayList(); } } );
for (OptionMetadata option : options) {
metadataIndex.put(option, option);
}
@@ -218,7 +286,7 @@ public OptionMetadata apply(@Nullable Collection options)
}
}));
- Map optionIndex = newHashMap();
+ Map optionIndex = Maps.newLinkedHashMap();
for (OptionMetadata option : options) {
for (String optionName : option.getOptions()) {
if (optionIndex.containsKey(optionName)) {
@@ -239,6 +307,98 @@ private static ImmutableList concat(Iterable iterable, T item)
return ImmutableList.builder().addAll(iterable).add(item).build();
}
+ public static void loadCommandsIntoGroupsByAnnotation(List allCommands, List commandGroups, List defaultCommandGroup)
+ {
+ List newCommands = new ArrayList();
+
+ // first, create any groups explicitly annotated
+ createGroupsFromAnnotations(allCommands,newCommands,commandGroups,defaultCommandGroup);
+
+ for (CommandMetadata command : allCommands) {
+ boolean added = false;
+
+ //now add the command to any groupNames specified in the Command annotation
+ for(String groupName : command.getGroupNames())
+ {
+ CommandGroupMetadata group = find(commandGroups, compose(equalTo(groupName), CommandGroupMetadata.nameGetter()), null);
+ if (group != null) {
+ group.addCommand(command);
+ added = true;
+ }
+ else
+ {
+ ImmutableList.Builder groupOptionsBuilder = ImmutableList.builder();
+ groupOptionsBuilder.addAll(command.getGroupOptions());
+ CommandGroupMetadata newGroup = loadCommandGroup(groupName,"",null, Collections.singletonList(command));
+ commandGroups.add(newGroup);
+ added = true;
+ }
+ }
+
+ if(added && defaultCommandGroup.contains(command))
+ {
+ defaultCommandGroup.remove(command);
+ }
+ }
+
+ allCommands.addAll(newCommands);
+ }
+
+ private static void createGroupsFromAnnotations(List allCommands, List newCommands, List commandGroups, List defaultCommandGroup)
+ {
+ for (CommandMetadata command : allCommands) {
+ boolean added = false;
+
+ // first, create any groups explicitly annotated
+ for(Group groupAnno : command.getGroups())
+ {
+ Class defaultCommandClass = null;
+ CommandMetadata defaultCommand = null;
+
+ //load default command if needed
+ if(!groupAnno.defaultCommand().equals(Group.DEFAULT.class))
+ {
+ defaultCommandClass = groupAnno.defaultCommand();
+ defaultCommand = find(allCommands, compose(equalTo(defaultCommandClass), CommandMetadata.typeGetter()), null);
+ if(null == defaultCommand)
+ {
+ defaultCommand = loadCommand(defaultCommandClass);
+ newCommands.add(defaultCommand);
+ }
+ }
+
+ //load other commands if needed
+ List groupCommands = new ArrayList(groupAnno.commands().length);
+ CommandMetadata groupCommand = null;
+ for(Class commandClass : groupAnno.commands())
+ {
+ groupCommand = find(allCommands, compose(equalTo(commandClass), CommandMetadata.typeGetter()), null);
+ if(null == groupCommand)
+ {
+ groupCommand = loadCommand(commandClass);
+ newCommands.add(groupCommand);
+ groupCommands.add(groupCommand);
+ }
+ }
+
+ CommandGroupMetadata groupMetadata = find(commandGroups, compose(equalTo(groupAnno.name()), CommandGroupMetadata.nameGetter()), null);
+ if(null == groupMetadata)
+ {
+ groupMetadata = loadCommandGroup(groupAnno.name(),groupAnno.description(),defaultCommand, groupCommands);
+ commandGroups.add(groupMetadata);
+ }
+
+ groupMetadata.addCommand(command);
+ added = true;
+ }
+
+ if(added && defaultCommandGroup.contains(command))
+ {
+ defaultCommandGroup.remove(command);
+ }
+ }
+ }
+
private static class InjectionMetadata
{
private List globalOptions = newArrayList();
diff --git a/src/main/java/io/airlift/command/model/OptionMetadata.java b/src/main/java/io/airlift/command/model/OptionMetadata.java
index 05f42d824..c5d04e170 100644
--- a/src/main/java/io/airlift/command/model/OptionMetadata.java
+++ b/src/main/java/io/airlift/command/model/OptionMetadata.java
@@ -84,8 +84,7 @@ public OptionMetadata(Iterable options)
Set accessors = newHashSet();
for (OptionMetadata other : options) {
- Preconditions.checkArgument(option.equals(other),
- "Conflicting options definitions: %s, %s", option, other);
+ Preconditions.checkArgument(option.equals(other), "Conflicting options definitions: %s, %s", option, other);
accessors.addAll(other.getAccessors());
}
diff --git a/src/test/java/io/airlift/command/AllTests.java b/src/test/java/io/airlift/command/AllTests.java
new file mode 100644
index 000000000..6fa3ce671
--- /dev/null
+++ b/src/test/java/io/airlift/command/AllTests.java
@@ -0,0 +1,34 @@
+package io.airlift.command;
+
+import io.airlift.command.command.CommandGroupAnnotationTest;
+import org.testng.TestNG;
+import org.testng.annotations.BeforeSuite;
+
+/**
+ *
+ *
+ * @author Michael Grove
+ * @since 0.6
+ * @version 0.6
+ */
+public class AllTests {
+
+ @BeforeSuite
+ public void testSuite() {
+ TestNG aTestNG = new TestNG();
+
+ aTestNG.setTestClasses(new Class[] { CommandTest.class, io.airlift.command.CommandTest.class,
+ ParametersDelegateTest.class, HelpTest.class, GitTest.class, GalaxyCommandLineParser.class,
+ CommandGroupAnnotationTest.class });
+
+ aTestNG.run();
+
+ if (aTestNG.hasFailure()) {
+ System.exit(1);
+ }
+ }
+
+ public static void main(String[] args) {
+ new AllTests().testSuite();
+ }
+}
diff --git a/src/test/java/io/airlift/command/GalaxyCommandLineParser.java b/src/test/java/io/airlift/command/GalaxyCommandLineParser.java
index 639bfc5a4..9175e07e7 100644
--- a/src/test/java/io/airlift/command/GalaxyCommandLineParser.java
+++ b/src/test/java/io/airlift/command/GalaxyCommandLineParser.java
@@ -1,7 +1,7 @@
package io.airlift.command;
import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists;
import io.airlift.command.Cli.CliBuilder;
import org.testng.annotations.Test;
@@ -91,7 +91,7 @@ public static class GlobalOptions
public boolean debug = false;
@Option(type = GLOBAL, name = "--coordinator", description = "Galaxy coordinator host (overrides GALAXY_COORDINATOR)")
- public String coordinator = Objects.firstNonNull(System.getenv("GALAXY_COORDINATOR"), "http://localhost:64000");
+ public String coordinator = MoreObjects.firstNonNull(System.getenv("GALAXY_COORDINATOR"), "http://localhost:64000");
@Override
public String toString()
diff --git a/src/test/java/io/airlift/command/Git.java b/src/test/java/io/airlift/command/Git.java
index 4f9ebd92e..2a0d1b1d8 100644
--- a/src/test/java/io/airlift/command/Git.java
+++ b/src/test/java/io/airlift/command/Git.java
@@ -64,7 +64,7 @@ public static class RemoteAdd extends GitCommand
@Option(name = "-t", description = "Track only a specific branch")
public String branch;
- @Arguments(description = "Remote repository to add")
+ @Arguments(description = "Name and URL of remote repository to add", title={"name", "url"})
public List remote;
}
}
diff --git a/src/test/java/io/airlift/command/GitTest.java b/src/test/java/io/airlift/command/GitTest.java
index 72a195394..1afd6dcfa 100644
--- a/src/test/java/io/airlift/command/GitTest.java
+++ b/src/test/java/io/airlift/command/GitTest.java
@@ -12,6 +12,11 @@ public void test()
git("add", "-p", "file");
git("remote", "add", "origin", "git@github.com:airlift/airline.git");
git("-v", "remote", "show", "origin");
+ // test default command
+ git("remote");
+ git("remote", "origin");
+ git("remote", "-n", "origin");
+ git("-v", "remote", "origin");
// show help
git();
diff --git a/src/test/java/io/airlift/command/HelpTest.java b/src/test/java/io/airlift/command/HelpTest.java
index dd2be7a7d..a7c112873 100644
--- a/src/test/java/io/airlift/command/HelpTest.java
+++ b/src/test/java/io/airlift/command/HelpTest.java
@@ -22,6 +22,7 @@
import io.airlift.command.Git.Add;
import io.airlift.command.Git.RemoteAdd;
import io.airlift.command.Git.RemoteShow;
+
import io.airlift.command.args.Args1;
import io.airlift.command.args.Args2;
import io.airlift.command.args.ArgsArityString;
@@ -29,18 +30,22 @@
import io.airlift.command.args.ArgsInherited;
import io.airlift.command.args.ArgsRequired;
import io.airlift.command.args.CommandHidden;
+import io.airlift.command.args.GlobalOptionsHidden;
import io.airlift.command.args.OptionsHidden;
import io.airlift.command.args.OptionsRequired;
+import io.airlift.command.command.CommandRemove;
+
import org.testng.Assert;
import org.testng.annotations.Test;
+import static io.airlift.command.Cli.buildCli;
import static io.airlift.command.SingleCommand.singleCommand;
@Test
public class HelpTest
{
- @Test
- public void testGit()
+ @SuppressWarnings("unchecked")
+ public void testGit()
{
CliBuilder builder = Cli.builder("git")
.withDescription("the stupid content tracker")
@@ -58,9 +63,9 @@ public void testGit()
StringBuilder out = new StringBuilder();
Help.help(gitParser.getMetadata(), ImmutableList.of(), out);
- Assert.assertEquals(out.toString(), "usage: git [-v] []\n" +
+ Assert.assertEquals(out.toString(), "usage: git [ -v ] [ ]\n" +
"\n" +
- "The most commonly used git commands are:\n" +
+ "Commands are:\n" +
" add Add file contents to the index\n" +
" help Display help information\n" +
" remote Manage set of tracked repositories\n" +
@@ -73,7 +78,7 @@ public void testGit()
" git add - Add file contents to the index\n" +
"\n" +
"SYNOPSIS\n" +
- " git [-v] add [-i] [--] [...]\n" +
+ " git [ -v ] add [ -i ] [--] [ ... ]\n" +
"\n" +
"OPTIONS\n" +
" -i\n" +
@@ -97,27 +102,60 @@ public void testGit()
" git remote - Manage set of tracked repositories\n" +
"\n" +
"SYNOPSIS\n" +
- " git [-v] remote\n" +
- " git [-v] remote add [-t ]\n" +
- " git [-v] remote show [-n]\n" +
+ " git [ -v ] remote { add | show* } [--] [cmd-options] \n" +
"\n" +
+ " Where command-specific options [cmd-options] are:\n" +
+ " add: [ -t ]\n" +
+ " show: [ -n ]\n" +
+ "\n" +
+ " Where command-specific arguments are:\n" +
+ " add: [ ... ]\n" +
+ " show: [ ]\n" +
+ "\n" +
+ " * show is the default command\n" +
+ " See 'git help remote ' for more information on a specific command.\n" +
"OPTIONS\n" +
" -v\n" +
" Verbose mode\n" +
+ "\n");
+// "COMMANDS\n" +
+// " By default, Gives some information about the remote \n" +
+// "\n" +
+// " show\n" +
+// " Gives some information about the remote \n" +
+// "\n" +
+// " With -n option, Do not query remote heads\n" +
+// "\n" +
+// " add\n" +
+// " Adds a remote\n" +
+// "\n" +
+// " With -t option, Track only a specific branch\n" +
+// "\n");
+
+ out = new StringBuilder();
+ Help.help(gitParser.getMetadata(), ImmutableList.of("remote", "add"), out);
+ Assert.assertEquals(out.toString(), "NAME\n" +
+ " git remote add - Adds a remote\n" +
"\n" +
- "COMMANDS\n" +
- " With no arguments, Gives some information about the remote \n" +
+ "SYNOPSIS\n" +
+ " git [ -v ] remote add [ -t ] [--] [ ... ]\n" +
"\n" +
- " show\n" +
- " Gives some information about the remote \n" +
+ "OPTIONS\n" +
+ " -t \n" +
+ " Track only a specific branch\n" +
"\n" +
- " With -n option, Do not query remote heads\n" +
+ " -v\n" +
+ " Verbose mode\n" +
"\n" +
- " add\n" +
- " Adds a remote\n" +
+ " --\n" +
+ " This option can be used to separate command-line options from the\n" +
+ " list of argument, (useful when arguments might be mistaken for\n" +
+ " command-line options\n" +
"\n" +
- " With -t option, Track only a specific branch\n" +
- "\n");
+ " \n" +
+ " Name and URL of remote repository to add\n" +
+ "\n"
+ );
}
@Test
@@ -137,10 +175,9 @@ public void testArgs1()
" test Args1 - args1 description\n" +
"\n" +
"SYNOPSIS\n" +
- " test Args1 [-bigdecimal ] [-date ] [-debug] [-double ]\n" +
- " [-float ] [-groups ]\n" +
- " [(-log