diff --git a/.gitignore b/.gitignore index 8b226763e..dc7d3c6f8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,12 @@ atlassian-ide-plugin.xml .classpath .project .settings -.project temp-testng-customsuite.xml .externalToolBuilders *~ .rvmrc +bin/ + +build/ +.gradle/ +gradle.properties \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dff5f3a5d..000000000 --- a/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: java diff --git a/README.md b/README.md index ba4fb09c3..d09a84d4d 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,6 @@ Airline Airline is a Java annotation-based framework for parsing Git like command line structures. -Latest release is 0.6, available from Maven Central. - -```xml - - io.airlift - airline - 0.6 - -``` - Here is a quick example: ```java @@ -289,3 +279,20 @@ OPTIONS -h, --help Display help information ``` + +**Updating manpages** +=================== + +1: In `~/.gradle.properites` : (example url is for adjusting manpages) +``` +artifactoryUrl=https://stardog.jfrog.io/stardog/cp-internal +artifactoryUsername=...... +artifactoryPassword=...... +``` +2: In airline and stardog root level `build.gradle` Update airline's version number + +3: in airline repo: `./gradlew clean install` + +4: in stardog repo: `./gradlew clean dist` (only do this once) + +5: for manpage generation - `./gradlew manPages`. (needs ronn) diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..c96c75f70 --- /dev/null +++ b/build.gradle @@ -0,0 +1,118 @@ +apply plugin: 'java' +apply plugin: 'idea' +apply plugin: "maven" + +tasks.create(name: 'wrapper_task' ,type: Wrapper) { + gradleVersion = '6.0.1' + distributionUrl = "https://services.gradle.org/distributions/gradle-6.0.1-all.zip" +} + +allprojects { + group = "com.complexible.airline" + sourceCompatibility = '1.8' + targetCompatibility = '1.8' + version = "0.8.1" + + repositories { + mavenCentral() + } +} + +ext { + projectDescription = "Airline is a Java annotation-based framework for parsing Git like command line structures." + projectUrl = "https://github.com/clarkparsia/airline" +} + + +dependencies { + compile 'javax.inject:javax.inject:1' + compile "com.google.guava:guava:27.0-jre" + compile 'com.google.code.findbugs:jsr305:3.0.0' + + testCompile 'org.testng:testng:6.0.1' +} + +if (project.hasProperty('artifactoryUrl') + && project.hasProperty('artifactoryUsername') + && project.hasProperty('artifactoryPassword')) { + + repoUpload(taskName: 'uploadArtifactory', + url: artifactoryUrl, + username: artifactoryUsername, + password: artifactoryPassword) +} + +if (project.hasProperty('publicRepository')) { + repoUpload(taskName: 'uploadGithub', + url: "file://localhost/$publicRepository") +} + +def repoUpload(Map options) { + def taskName = options.taskName + def repoUrl = options.url + def username = null + def passwd = null + def auth = options.containsKey("username") && options.containsKey("password") + if (auth) { + username = options.username + passwd = options.password + } + + task(taskName, type: Upload) { + configuration = configurations.archives + repositories { + mavenDeployer { + repository(url: repoUrl) { + if (auth) { + authentication(userName: username, password: passwd) + } + } + pom.project { + name = archivesBaseName + packaging = 'jar' + description projectDescription + url projectUrl + } + + //mess with the generated pom to remove test dependencies from published artifacts + pom.withXml { XmlProvider xmlProvider -> + def xml = xmlProvider.asString() + def pomXml = new XmlParser().parse(new ByteArrayInputStream(xml.toString().bytes)) + + pomXml.dependencies.dependency.each { dep -> + if (dep.scope.text() != 'compile') { + def parent = dep.parent() + parent.remove(dep) + } + } + + // add exclusion nodes (only for compile conf) since the maven plugin + // doesn't handle them as of gradle 1.9 + project.configurations.compile.allDependencies.findAll { + it instanceof ModuleDependency && !it.excludeRules.isEmpty() + }.each { ModuleDependency dep -> + def xmlDep = pomXml.dependencies.dependency.find { + it.groupId[0].text() == dep.group && it.artifactId[0].text() == dep.name + } + def xmlExclusions = xmlDep.exclusions + if (!xmlExclusions) xmlExclusions = xmlDep.appendNode('exclusions') + + dep.excludeRules.each { ExcludeRule rule -> + def xmlExclusion = xmlExclusions.appendNode('exclusion') + xmlExclusion.appendNode('groupId', rule.group) + xmlExclusion.appendNode('artifactId', rule.module ?: '*') + } + } + + def newXml = new StringWriter() + def printer = new XmlNodePrinter(new PrintWriter(newXml)) + printer.preserveWhitespace = true + printer.print(pomXml) + xml.setLength(0) + xml.append(newXml.toString()) + } + + } + } + } +} diff --git a/gradle.properties.example b/gradle.properties.example new file mode 100644 index 000000000..b2812fd14 --- /dev/null +++ b/gradle.properties.example @@ -0,0 +1,4 @@ +publicRepository=/path/to/your/maven/checkout +artifactoryUrl=/url/to/repo +artifactoryUsername=user +artifactoryPassword=secret \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..d5c591c9c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..de22e8173 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 11 10:07:24 EST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/notice.md b/notice.md index 87ca1a252..ef852077b 100644 --- a/notice.md +++ b/notice.md @@ -1,6 +1,8 @@ Copyright Notices ================= +Copyright 2013 Fernando Hernandez +Copyright 2013 Michael Grove Copyright 2011 Dain Sundstrom Copyright 2010 Cedric Beust diff --git a/pom.xml b/pom.xml deleted file mode 100644 index d0efc1644..000000000 --- a/pom.xml +++ /dev/null @@ -1,184 +0,0 @@ - - - - 4.0.0 - io.airlift - airline - jar - cli - 0.7-SNAPSHOT - - Airline is a Java annotation-based framework for parsing Git like command line structures. - https://github.com/airlift/airline - - 2012 - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - - dain - Dain Sundstrom - dain@iq80.com - - - - - scm:git:git@github.com:airlift/airline.git - scm:git:git@github.com:airlift/airline.git - http://github.com/airlift/airline/tree/master - - - - UTF-8 - - - - - sonatype-nexus-snapshots - Sonatype Nexus Snapshots - https://oss.sonatype.org/content/repositories/snapshots/ - - - sonatype-nexus-staging - Nexus Release Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - javax.inject - javax.inject - 1 - - - - com.google.guava - guava - 12.0 - - - - org.testng - testng - 6.0.1 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - - org.apache.maven.plugins - maven-source-plugin - 2.1.2 - - true - - - - create-source-jar - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.7 - - - - org.apache.maven.plugins - maven-release-plugin - 2.2.1 - - forked-path - -Psonatype-oss-release - false - false - true - clean install - @{project.version} - - - - - - - - sonatype-oss-release - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.1 - - - sign-artifacts - verify - - sign - - - true - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - - jar - - - - - - - - - diff --git a/src/main/java/io/airlift/command/Arguments.java b/src/main/java/io/airlift/command/Arguments.java index 8cdf512b2..b726b6364 100644 --- a/src/main/java/io/airlift/command/Arguments.java +++ b/src/main/java/io/airlift/command/Arguments.java @@ -28,9 +28,9 @@ public @interface Arguments { /** - * Name of the arguments. + * Name or names of the arguments. */ - String title() default ""; + String[] title() default {""}; /** * A description of the arguments. @@ -43,7 +43,7 @@ String usage() default ""; /** - * Whether this arguments are required. + * Whether these arguments are required. */ boolean required() default false; } diff --git a/src/main/java/io/airlift/command/Cli.java b/src/main/java/io/airlift/command/Cli.java index f83500d22..0df992d2c 100644 --- a/src/main/java/io/airlift/command/Cli.java +++ b/src/main/java/io/airlift/command/Cli.java @@ -23,6 +23,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + import io.airlift.command.model.ArgumentsMetadata; import io.airlift.command.model.CommandGroupMetadata; import io.airlift.command.model.CommandMetadata; @@ -30,6 +32,7 @@ import io.airlift.command.model.MetadataLoader; import io.airlift.command.model.OptionMetadata; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -59,32 +62,53 @@ public static CliBuilder buildCli(String name, Class commandTypes) private final GlobalMetadata metadata; + private final CommandFactory mCommandFactory; + private Cli(String name, String description, TypeConverter typeConverter, Class defaultCommand, + CommandFactory theCommandFactory, Iterable> defaultGroupCommands, Iterable> groups) { Preconditions.checkNotNull(name, "name is null"); Preconditions.checkNotNull(typeConverter, "typeConverter is null"); + Preconditions.checkNotNull(theCommandFactory); + + mCommandFactory = theCommandFactory; CommandMetadata defaultCommandMetadata = null; if (defaultCommand != null) { defaultCommandMetadata = MetadataLoader.loadCommand(defaultCommand); } - List defaultCommandGroup = MetadataLoader.loadCommands(defaultGroupCommands); + final List allCommands = new ArrayList(); + + List defaultCommandGroup = Lists.newArrayList(MetadataLoader.loadCommands(defaultGroupCommands)); - List commandGroups = ImmutableList.copyOf(Iterables.transform(groups, new Function, CommandGroupMetadata>() + // currentlly the default command is required to be in the commands list. If that changes, we'll need to add it here and add checks for existence + allCommands.addAll(defaultCommandGroup); + + List commandGroups = Lists.newArrayList(Iterables.transform(groups, new Function, CommandGroupMetadata>() { public CommandGroupMetadata apply(GroupBuilder group) { - return MetadataLoader.loadCommandGroup(group.name, group.description, MetadataLoader.loadCommand(group.defaultCommand), MetadataLoader.loadCommands(group.commands)); + CommandMetadata groupDefault = MetadataLoader.loadCommand(group.defaultCommand); + List groupCommands = MetadataLoader.loadCommands(group.commands); + + // currentlly the default command is required to be in the commands list. If that changes, we'll need to add it here and add checks for existence + allCommands.addAll(groupCommands); + + return MetadataLoader.loadCommandGroup(group.name, group.description, groupDefault, groupCommands); } })); - this.metadata = MetadataLoader.loadGlobal(name, description, defaultCommandMetadata, defaultCommandGroup, commandGroups); + // add commands to groups based on the value of groups in the @Command annotations + // rather than change the entire way metadata is loaded, I figured just post-processing was an easier, yet uglier, way to go + MetadataLoader.loadCommandsIntoGroupsByAnnotation(allCommands,commandGroups, defaultCommandGroup); + + this.metadata = MetadataLoader.loadGlobal(name, description, defaultCommandMetadata, ImmutableList.copyOf(defaultCommandGroup), ImmutableList.copyOf(commandGroups)); } public GlobalMetadata getMetadata() @@ -92,12 +116,22 @@ public GlobalMetadata getMetadata() return metadata; } + public C parse(CommandFactory commandFactory, String... args) + { + return parse(commandFactory, ImmutableList.copyOf(args)); + } + public C parse(String... args) { - return parse(ImmutableList.copyOf(args)); + return parse(mCommandFactory, ImmutableList.copyOf(args)); } - - public C parse(Iterable args) + + public C parse(Iterable args) + { + return parse(mCommandFactory, args); + } + + public C parse(CommandFactory commandFactory, Iterable args) { Preconditions.checkNotNull(args, "args is null"); @@ -117,13 +151,56 @@ public C parse(Iterable args) CommandMetadata command = state.getCommand(); + ImmutableMap.Builder, Object> bindings = ImmutableMap., Object>builder().put(GlobalMetadata.class, metadata); + + if (state.getGroup() != null) { + bindings.put(CommandGroupMetadata.class, state.getGroup()); + } + + if (state.getCommand() != null) { + bindings.put(CommandMetadata.class, state.getCommand()); + } + return createInstance(command.getType(), command.getAllOptions(), state.getParsedOptions(), command.getArguments(), state.getParsedArguments(), command.getMetadataInjections(), - ImmutableMap., Object>of(GlobalMetadata.class, metadata)); + bindings.build(), + commandFactory); + } + + public C parse(C commandInstance, String... args) + { + Preconditions.checkNotNull(args, "args is null"); + + Parser parser = new Parser(); + ParseState state = parser.parse(metadata, args); + + CommandMetadata command = MetadataLoader.loadCommand(commandInstance.getClass()); + + state = state.withCommand(command); + + validate(state); + + ImmutableMap.Builder, Object> bindings = ImmutableMap., Object>builder().put(GlobalMetadata.class, metadata); + + if (state.getGroup() != null) { + bindings.put(CommandGroupMetadata.class, state.getGroup()); + } + + bindings.put(CommandMetadata.class, command); + + C c = (C) ParserUtil.injectOptions(commandInstance, + command.getAllOptions(), + state.getParsedOptions(), + command.getArguments(), + state.getParsedArguments(), + command.getMetadataInjections(), + bindings.build()); + + return c; } private void validate(ParseState state) @@ -172,6 +249,7 @@ public static class CliBuilder private Class defaultCommand; private final List> defaultCommandGroupCommands = newArrayList(); protected final Map> groups = newHashMap(); + protected CommandFactory commandFactory = new CommandFactoryDefault(); public CliBuilder(String name) { @@ -187,6 +265,12 @@ public CliBuilder withDescription(String description) this.description = description; return this; } + + public CliBuilder withCommandFactory(CommandFactory commandFactory) + { + this.commandFactory = commandFactory; + return this; + } // public CliBuilder withTypeConverter(TypeConverter typeConverter) // { @@ -214,7 +298,8 @@ public CliBuilder withCommand(Class command) return this; } - public CliBuilder withCommands(Class command, Class... moreCommands) + @SafeVarargs + public final CliBuilder withCommands(Class command, Class... moreCommands) { this.defaultCommandGroupCommands.add(command); this.defaultCommandGroupCommands.addAll(ImmutableList.copyOf(moreCommands)); @@ -241,9 +326,17 @@ public GroupBuilder withGroup(String name) return group; } + public GroupBuilder getGroup(final String theName) { + Preconditions.checkNotNull(theName, "name is null"); + Preconditions.checkArgument(!theName.isEmpty(), "name is empty"); + Preconditions.checkArgument(groups.containsKey(theName), "Group %s has not been declared", theName); + + return groups.get(theName); + } + public Cli build() { - return new Cli(name, description, typeConverter, defaultCommand, defaultCommandGroupCommands, groups.values()); + return new Cli(name, description, typeConverter, defaultCommand, commandFactory, defaultCommandGroupCommands, groups.values()); } } @@ -285,7 +378,8 @@ public GroupBuilder withCommand(Class command) return this; } - public GroupBuilder withCommands(Class command, Class... moreCommands) + @SafeVarargs + public final GroupBuilder withCommands(Class command, Class... moreCommands) { this.commands.add(command); this.commands.addAll(ImmutableList.copyOf(moreCommands)); diff --git a/src/main/java/io/airlift/command/Command.java b/src/main/java/io/airlift/command/Command.java index da0734f86..61bea7882 100644 --- a/src/main/java/io/airlift/command/Command.java +++ b/src/main/java/io/airlift/command/Command.java @@ -47,4 +47,31 @@ * If true, this command won't appear in the usage(). */ boolean hidden() default false; + + /** + * An array of lines of text to provide a series of example usages of the command. + * + * {@code + examples = {"* Explain what the command example does", + " $ cli group cmd foo.txt file.json", + "", + "* Explain what this command example does", + " $ cli group cmd --non-standard-option value foo.txt"} + } + * Formatting and blank lines are preserved to give users leverage over how the examples are displayed in the usage. + */ + String[] examples() default {}; + + /** + * Block of text that provides an extended discussion on the behavior of the command. Should + * supplement the shorter description which is more of a summary where discussion can get into + * greater detail. + */ + String discussion() default ""; + + /** + * the group(s) this command should belong to. + * if left empty the command will belong to the default command group + */ + String[] groupNames() default {}; } diff --git a/src/main/java/io/airlift/command/CommandFactory.java b/src/main/java/io/airlift/command/CommandFactory.java new file mode 100644 index 000000000..37a39cdbe --- /dev/null +++ b/src/main/java/io/airlift/command/CommandFactory.java @@ -0,0 +1,5 @@ +package io.airlift.command; + +public interface CommandFactory { + T createInstance(Class type); +} diff --git a/src/main/java/io/airlift/command/CommandFactoryDefault.java b/src/main/java/io/airlift/command/CommandFactoryDefault.java new file mode 100644 index 000000000..3e9a9d713 --- /dev/null +++ b/src/main/java/io/airlift/command/CommandFactoryDefault.java @@ -0,0 +1,10 @@ +package io.airlift.command; + +public class CommandFactoryDefault implements CommandFactory { + + @Override + public T createInstance(Class type) { + return (T) ParserUtil.createInstance(type); + } + +} diff --git a/src/main/java/io/airlift/command/CommandGroupUsage.java b/src/main/java/io/airlift/command/CommandGroupUsage.java index 4ef237501..8ed1f1395 100644 --- a/src/main/java/io/airlift/command/CommandGroupUsage.java +++ b/src/main/java/io/airlift/command/CommandGroupUsage.java @@ -1,17 +1,21 @@ package io.airlift.command; import com.google.common.base.Preconditions; +import com.google.common.base.Objects; +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.collect.ImmutableList; import io.airlift.command.model.CommandGroupMetadata; import io.airlift.command.model.CommandMetadata; import io.airlift.command.model.GlobalMetadata; import io.airlift.command.model.OptionMetadata; +import io.airlift.command.model.ArgumentsMetadata; import javax.annotation.Nullable; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import java.util.*; import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Maps.newTreeMap; import static io.airlift.command.UsageHelper.DEFAULT_COMMAND_COMPARATOR; import static io.airlift.command.UsageHelper.DEFAULT_OPTION_COMPARATOR; @@ -87,29 +91,99 @@ public void usage(@Nullable GlobalMetadata global, CommandGroupMetadata group, U List commands = newArrayList(group.getCommands()); Collections.sort(commands, commandComparator); + // Populate group info via an extra for loop through commands + String defaultCommand = ""; if (group.getDefaultCommand() != null) { - CommandMetadata command = group.getDefaultCommand(); - if (global != null) { - synopsis.append(global.getName()); - if (!hideGlobalOptions) { - synopsis.appendWords(UsageHelper.toSynopsisUsage(command.getGlobalOptions())); - } + defaultCommand = group.getDefaultCommand().getName(); + } + List commonGroupOptions = null; + String commonGroupArgs = null; + List allCommandNames = newArrayList(); + boolean hasCommandSpecificOptions = false, hasCommandSpecificArgs = false; + for (CommandMetadata command : commands) { + if (command.getName().equals(defaultCommand)) { + allCommandNames.add(command.getName() + "*"); + } + else { + allCommandNames.add(command.getName()); + } + if (commonGroupOptions == null) { + commonGroupOptions = newArrayList(command.getCommandOptions()); + } + if (commonGroupArgs == null) { + commonGroupArgs = (command.getArguments() != null ? UsageHelper.toUsage(command.getArguments()) : ""); + } + + commonGroupOptions.retainAll(command.getCommandOptions()); + if (command.getCommandOptions().size() > commonGroupOptions.size()) { + hasCommandSpecificOptions = true; + } + if (commonGroupArgs != (command.getArguments() != null ? UsageHelper.toUsage(command.getArguments()) : "")) { + hasCommandSpecificArgs = true; + } + } + // Print group summary line + if (global != null) { + synopsis.append(global.getName()); + if (!hideGlobalOptions) { + synopsis.appendWords(UsageHelper.toSynopsisUsage(commands.get(0).getGlobalOptions())); } - synopsis.append(group.getName()).appendWords(UsageHelper.toSynopsisUsage(command.getGroupOptions())); - synopsis.newline(); } + synopsis.append(group.getName()).appendWords(UsageHelper.toSynopsisUsage(commands.get(0).getGroupOptions())); + synopsis.append(" {").append(allCommandNames.get(0)); + for (int i = 1; i < allCommandNames.size(); i++) { + synopsis.append(" | ").append(allCommandNames.get(i)); + } + synopsis.append("} [--]"); + if (commonGroupOptions.size() > 0) { + synopsis.appendWords(UsageHelper.toSynopsisUsage(commonGroupOptions)); + } + if (hasCommandSpecificOptions) { + synopsis.append(" [cmd-options]"); + } + if (hasCommandSpecificArgs) { + synopsis.append(" "); + } + synopsis.newline(); + Map cmdOptions = newTreeMap(); + Map cmdArguments = newTreeMap(); + for (CommandMetadata command : commands) { - if (global != null) { - synopsis.append(global.getName()); - if (!hideGlobalOptions) { - synopsis.appendWords(UsageHelper.toSynopsisUsage(command.getGlobalOptions())); + + if(!command.isHidden()) + { + if (hasCommandSpecificOptions) { + List thisCmdOptions = newArrayList(command.getCommandOptions()); + thisCmdOptions.removeAll(commonGroupOptions); + StringBuilder optSB = new StringBuilder(); + for (String s : UsageHelper.toSynopsisUsage(thisCmdOptions)) { + optSB.append(s + " "); + } + cmdOptions.put(command.getName(), optSB.toString()); + } + if (hasCommandSpecificArgs) { + cmdArguments.put(command.getName(), (command.getArguments() != null ? UsageHelper.toUsage(command.getArguments()) : "")); } } - synopsis.append(group.getName()).appendWords(UsageHelper.toSynopsisUsage(command.getGroupOptions())); - synopsis.append(command.getName()).appendWords(UsageHelper.toSynopsisUsage(command.getCommandOptions())); - synopsis.newline(); } - synopsis.newline(); + if (hasCommandSpecificOptions) { + synopsis.newline().append("Where command-specific options [cmd-options] are:").newline(); + UsagePrinter opts = synopsis.newIndentedPrinter(4); + for (String cmd : cmdOptions.keySet()) { + opts.append(cmd + ": " + cmdOptions.get(cmd)).newline(); + } + } + if (hasCommandSpecificArgs) { + synopsis.newline().append("Where command-specific arguments are:").newline(); + UsagePrinter args = synopsis.newIndentedPrinter(4); + for (String arg : cmdArguments.keySet()) { + args.append(arg + ": " + cmdArguments.get(arg)).newline(); + } + } + if (defaultCommand != "") { + synopsis.newline().append(String.format("* %s is the default command", defaultCommand)); + } + synopsis.newline().append("See").append("'" + global.getName()).append("help ").append(group.getName()).appendOnOneLine(" ' for more information on a specific command.").newline(); // // OPTIONS @@ -127,6 +201,12 @@ public void usage(@Nullable GlobalMetadata global, CommandGroupMetadata group, U 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(); @@ -138,39 +218,6 @@ public void usage(@Nullable GlobalMetadata global, CommandGroupMetadata group, U descriptionPrinter.newline(); } } - - // - // COMMANDS - // - if (commands.size() > 0 || group.getDefaultCommand() != null) { - out.append("COMMANDS").newline(); - UsagePrinter commandPrinter = out.newIndentedPrinter(8); - - if (group.getDefaultCommand() != null && group.getDefaultCommand().getDescription() != null) { - commandPrinter.append("With no arguments,") - .append(group.getDefaultCommand().getDescription()) - .newline() - .newline(); - } - - for (CommandMetadata command : group.getCommands()) { - commandPrinter.append(command.getName()).newline(); - UsagePrinter descriptionPrinter = commandPrinter.newIndentedPrinter(4); - - descriptionPrinter.append(command.getDescription()).newline().newline(); - - for (OptionMetadata option : command.getCommandOptions()) { - if (!option.isHidden() && option.getDescription() != null) { - descriptionPrinter.append("With") - .append(longest(option.getOptions())) - .append("option,") - .append(option.getDescription()) - .newline() - .newline(); - } - } - } - } } private static String longest(Iterable iterable) diff --git a/src/main/java/io/airlift/command/CommandUsage.java b/src/main/java/io/airlift/command/CommandUsage.java index 6f84593b4..33a2db2fc 100644 --- a/src/main/java/io/airlift/command/CommandUsage.java +++ b/src/main/java/io/airlift/command/CommandUsage.java @@ -1,19 +1,26 @@ +// 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 com.google.common.base.Preconditions; +import static com.google.common.collect.Lists.newArrayList; +import static io.airlift.command.UsageHelper.DEFAULT_OPTION_COMPARATOR; +import static io.airlift.command.UsageHelper.toSynopsisUsage; import io.airlift.command.model.ArgumentsMetadata; import io.airlift.command.model.CommandMetadata; import io.airlift.command.model.OptionMetadata; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import static com.google.common.collect.Lists.newArrayList; -import static io.airlift.command.UsageHelper.DEFAULT_OPTION_COMPARATOR; -import static io.airlift.command.UsageHelper.toSynopsisUsage; +import javax.annotation.Nullable; + +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; public class CommandUsage { @@ -133,7 +140,7 @@ public void usage(@Nullable String programName, @Nullable String groupName, Stri "list of argument, (useful when arguments might be mistaken for command-line options").newline(); descriptionPrinter.newline(); - // arguments name + // arguments name(s) optionPrinter.append(UsageHelper.toDescription(arguments)).newline(); // description @@ -142,14 +149,438 @@ public void usage(@Nullable String programName, @Nullable String groupName, Stri } } + if (command.getDiscussion() != null) { + out.append("DISCUSSION").newline(); + UsagePrinter disc = out.newIndentedPrinter(8); + + disc.append(command.getDiscussion()) + .newline() + .newline(); + } + + if (command.getExamples() != null && !command.getExamples().isEmpty()) { + out.append("EXAMPLES").newline(); + UsagePrinter ex = out.newIndentedPrinter(8); + + ex.appendTable(Iterables.partition(command.getExamples(), 1)); + } } - private List sortOptions(List options) - { + private List sortOptions(List options) { if (optionComparator != null) { options = new ArrayList(options); Collections.sort(options, optionComparator); } return options; } + + public String usageRonn(@Nullable String programName, @Nullable String groupName, CommandMetadata command) { + final StringBuilder aBuilder = new StringBuilder(""); + + final String NEW_PARA = "\n\n"; + + aBuilder.append(programName).append("_"); + aBuilder.append(groupName).append("_"); + // stardog-admin commands go in section 8 (sysadmin commands), all others in section 1 (user commands) + aBuilder.append(command.getName()) + .append(programName != null && programName.equals("stardog-admin") ? "(8) -" : "(1) -"); + String aDescription = command.getDescription(); + String aLongDesc = null; + if (aBuilder.length() + aDescription.length() >= 255) { // some arbitrary length + // if description is too long, we'll try to get the first sentence then put the whole + // thing in the DESCRIPTION section + final int aFirstPeriod= aDescription.indexOf('.'); + if (aFirstPeriod != -1) { + String aShortDesc = aDescription.substring(0, aFirstPeriod + 1); + if (aBuilder.length() + aShortDesc.length() < 255) { + aBuilder.append(aShortDesc).append("\n"); + } + } + aLongDesc = aDescription; + } + else { + aBuilder.append(aDescription).append("\n"); + } + aBuilder.append("=========="); + + aBuilder.append(NEW_PARA).append("## SYNOPSIS").append(NEW_PARA); + List options = newArrayList(); + List aOptions; + if (programName != null) { + aBuilder.append("`").append(programName).append("`"); + aOptions = command.getGlobalOptions(); + if (aOptions != null && aOptions.size() > 0) { + aBuilder.append(" ").append(Joiner.on(" ").join(toSynopsisUsage(sortOptions(aOptions)))); + options.addAll(aOptions); + } + } + if (groupName != null) { + aBuilder.append(" `").append(groupName).append("`"); + aOptions = command.getGroupOptions(); + if (aOptions != null && aOptions.size() > 0) { + aBuilder.append(" ").append(Joiner.on(" ").join(toSynopsisUsage(sortOptions(aOptions)))); + options.addAll(aOptions); + } + } + aOptions = command.getCommandOptions(); + aBuilder.append(" `").append(command.getName()).append("` ").append(Joiner.on(" ").join(toSynopsisUsage(sortOptions(aOptions)))); + options.addAll(aOptions); + + // command arguments (optional) + ArgumentsMetadata arguments = command.getArguments(); + if (arguments != null) { + aBuilder.append(" [--] ") + .append(UsageHelper.toUsage(arguments)); + } + + if (aLongDesc != null) { + aBuilder.append(NEW_PARA).append("## DESCRIPTION").append(NEW_PARA).append(aLongDesc); + } + + + if (options.size() > 0 || arguments != null) { + aBuilder.append(NEW_PARA).append("## OPTIONS"); + options = sortOptions(options); + + for (OptionMetadata option : options) { + // skip hidden options + if (option.isHidden()) { + continue; + } + + // option names + aBuilder.append(NEW_PARA).append("* ").append(UsageHelper.toRonnDescription(option)).append(":\n"); + + // description + aBuilder.append(option.getDescription()); + } + + if (arguments != null) { + // "--" option + aBuilder.append(NEW_PARA).append("* --:\n"); + + // description + aBuilder.append("This option can be used to separate command-line options from the " + + "list of arguments (useful when arguments might be mistaken for command-line options)."); + + // arguments name + aBuilder.append(NEW_PARA).append("* ").append(UsageHelper.toDescription(arguments)).append(":\n"); + + // description + aBuilder.append(arguments.getDescription()); + } + } + + if (command.getDiscussion() != null) { + aBuilder.append(NEW_PARA).append("## DISCUSSION").append(NEW_PARA); + aBuilder.append(command.getDiscussion()); + } + + if (command.getExamples() != null && !command.getExamples().isEmpty()) { + aBuilder.append(NEW_PARA).append("## EXAMPLES"); + + // 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(NEW_PARA).append("* ").append(aText).append(":\n"); + aBuilder.append(aEx); + } + } + + return aBuilder.toString(); + } + + public String usageHTML(@Nullable String programName, @Nullable String groupName, CommandMetadata command) { + final StringBuilder aBuilder = new StringBuilder(""); + + final String NEWLINE = "
\n"; + // TODO need boostrap css + + // todo close this + aBuilder.append("\n"); + aBuilder.append("\n"); + aBuilder.append("\n"); + aBuilder.append("\n"); + aBuilder.append("\n"); + aBuilder.append("\n"); + + aBuilder.append("
\n"); + aBuilder.append("

").append(programName).append(" ").append(groupName).append(" ").append(command.getName()).append(" Manual Page\n"); + aBuilder.append("
\n"); + + aBuilder.append("

NAME

\n").append(NEWLINE); + + aBuilder.append("
"); + 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("

SYNOPSIS

\n").append(NEWLINE); + + List 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 + + // for jekyll to pick up these pages on the website + aBuilder.append("---\n"); + aBuilder.append("layout: default\n"); + aBuilder.append("title: ").append(groupName).append(" ").append(command.getName()).append("\n"); + + if (programName.equals("stardog")){ + aBuilder.append("grand_parent: ").append("Stardog CLI Reference\n"); + } + else { + aBuilder.append("grand_parent: ").append("Stardog Admin CLI Reference\n"); + } + + aBuilder.append("parent: ").append(groupName).append("\n"); + aBuilder.append("description: ").append("'").append(command.getDescription().replace("'","")).append("'").append("\n"); + aBuilder.append("---\n\n"); + + aBuilder.append("# ").append(" `").append(programName).append(" ").append(groupName).append(" ").append(command.getName()).append("` ").append("\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{: .fs-5}\n"); + + if (options.size() > 0 || arguments != null) { + options = sortOptions(options); + aBuilder.append("## Options\n\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("\n## Discussion\n").append(command.getDiscussion()).append("\n"); + } + + if (command.getExamples() != null && !command.getExamples().isEmpty()) { + aBuilder.append("\n## 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```bash\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..156339e4f 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,14 +78,25 @@ 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; } } // command in a group? for (CommandGroupMetadata group : global.getCommandGroups()) { - if (name.endsWith(group.getName())) { + if (name.equals(group.getName())) { // general group help or specific command help? if (commandNames.size() == 1) { new CommandGroupUsage().usage(global, group, out); @@ -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> 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)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 | -verbose )] [-long ] [--]\n" + - " [...]\n" + + " test Args1 [ -bigdecimal ] [ -date ] [ -debug ]\n" + + " [ -double ] [ -float ] [ -groups ]\n" + + " [ {-log | -verbose} ] [ -long ] [--] [ ... ]\n" + "\n" + "OPTIONS\n" + " -bigdecimal \n" + @@ -194,8 +231,8 @@ public void testArgs2() " test Args2 -\n" + "\n" + "SYNOPSIS\n" + - " test Args2 [-debug] [-groups ] [-host ...]\n" + - " [(-log | -verbose )] [--] [...]\n" + + " test Args2 [ -debug ] [ -groups ] [ -host ... ]\n" + + " [ {-log | -verbose} ] [--] [ ... ]\n" + "\n" + "OPTIONS\n" + " -debug\n" + @@ -237,7 +274,7 @@ public void testArgsAritySting() " test ArgsArityString -\n" + "\n" + "SYNOPSIS\n" + - " test ArgsArityString [-pairs ...] [--] [...]\n" + + " test ArgsArityString [ -pairs ... ] [--] [ ... ]\n" + "\n" + "OPTIONS\n" + " -pairs \n" + @@ -270,7 +307,7 @@ public void testArgsBooleanArity() " test ArgsBooleanArity -\n" + "\n" + "SYNOPSIS\n" + - " test ArgsBooleanArity [-debug ]\n" + + " test ArgsBooleanArity [ -debug ]\n" + "\n" + "OPTIONS\n" + " -debug \n" + @@ -295,8 +332,8 @@ public void testArgsInherited() " test ArgsInherited -\n" + "\n" + "SYNOPSIS\n" + - " test ArgsInherited [-child ] [-debug] [-groups ]\n" + - " [-level ] [-log ] [--] [...]\n" + + " test ArgsInherited [ -child ] [ -debug ] [ -groups ]\n" + + " [ -level ] [ -log ] [--] [ ... ]\n" + "\n" + "OPTIONS\n" + " -child \n" + @@ -371,7 +408,7 @@ public void testOptionsRequired() " test OptionsRequired -\n" + "\n" + "SYNOPSIS\n" + - " test OptionsRequired [--optional ]\n" + + " test OptionsRequired [ --optional ]\n" + " --required \n" + "\n" + "OPTIONS\n" + @@ -400,7 +437,7 @@ public void testOptionsHidden() " test OptionsHidden -\n" + "\n" + "SYNOPSIS\n" + - " test OptionsHidden [--optional ]\n" + + " test OptionsHidden [ --optional ]\n" + "\n" + "OPTIONS\n" + " --optional \n" + @@ -408,6 +445,31 @@ public void testOptionsHidden() "\n"); } + @Test + public void testGlobalOptionsHidden() + { + CliBuilder builder = buildCli("test", Object.class) + .withDescription("Test commandline") + .withDefaultCommand(Help.class) + .withCommands(Help.class, + GlobalOptionsHidden.class); + + Cli parser = builder.build(); + + StringBuilder out = new StringBuilder(); + Help.help(parser.getMetadata(), ImmutableList.of("GlobalOptionsHidden"), out); + Assert.assertEquals(out.toString(), "NAME\n" + + " test GlobalOptionsHidden -\n" + + "\n" + + "SYNOPSIS\n" + + " test [ {-op | --optional} ] GlobalOptionsHidden\n" + + "\n" + + "OPTIONS\n" + + " -op, --optional\n" + + "\n" + + "\n"); + } + @Test public void testCommandHidden() { @@ -421,9 +483,9 @@ public void testCommandHidden() StringBuilder out = new StringBuilder(); Help.help(parser.getMetadata(), ImmutableList.of(), out); - Assert.assertEquals(out.toString(), "usage: test []\n" + + Assert.assertEquals(out.toString(), "usage: test [ ]\n" + "\n" + - "The most commonly used test commands are:\n" + + "Commands are:\n" + " ArgsRequired\n" + " help Display help information\n" + "\n" + @@ -435,7 +497,7 @@ public void testCommandHidden() " test CommandHidden -\n" + "\n" + "SYNOPSIS\n" + - " test CommandHidden [--optional ]\n" + + " test CommandHidden [ --optional ]\n" + "\n" + "OPTIONS\n" + " --optional \n" + @@ -444,6 +506,27 @@ public void testCommandHidden() } + @Test + public void testExamplesAndDiscussion() { + Cli parser = Cli.builder("git") + .withCommand(CommandRemove.class) + .build(); + + StringBuilder out = new StringBuilder(); + Help.help(parser.getMetadata(), ImmutableList.of("remove"), out); + + String discussion = "DISCUSSION\n" + + " More details about how this removes files from the index.\n" + + "\n"; + + String examples = "EXAMPLES\n" + + " * The following is a usage example:\n" + + " \t$ git remove -i myfile.java\n"; + + Assert.assertTrue(out.toString().contains(discussion), "Expected the discussion section to be present in the help"); + Assert.assertTrue(out.toString().contains(examples), "Expected the examples section to be present in the help"); + } + @Test public void testSingleCommandArgs1() { @@ -455,10 +538,9 @@ public void testSingleCommandArgs1() " test - args1 description\n" + "\n" + "SYNOPSIS\n" + - " test [-bigdecimal ] [-date ] [-debug] [-double ]\n" + - " [-float ] [-groups ]\n" + - " [(-log | -verbose )] [-long ] [--]\n" + - " [...]\n" + + " test [ -bigdecimal ] [ -date ] [ -debug ]\n" + + " [ -double ] [ -float ] [ -groups ]\n" + + " [ {-log | -verbose} ] [ -long ] [--] [ ... ]\n" + "\n" + "OPTIONS\n" + " -bigdecimal \n" + @@ -494,4 +576,21 @@ public void testSingleCommandArgs1() "\n" + "\n"); } + + @Test + public void testUnknownCommand() { + CliBuilder builder = Cli.builder("test") + .withDescription("Test commandline") + .withDefaultCommand(Help.class) + .withCommands(Help.class, + OptionsRequired.class); + try { + Help help = (Help) builder.build().parse("asdf"); + help.call(); + Assert.fail("Exception should have been thrown for unknown command"); + } + catch (UnsupportedOperationException e) { + Assert.assertEquals("Unknown command asdf", e.getMessage()); + } + } } diff --git a/src/test/java/io/airlift/command/SingleCommandTest.java b/src/test/java/io/airlift/command/SingleCommandTest.java index a413a8e59..f302b6441 100644 --- a/src/test/java/io/airlift/command/SingleCommandTest.java +++ b/src/test/java/io/airlift/command/SingleCommandTest.java @@ -22,6 +22,7 @@ import io.airlift.command.Cli.CliBuilder; import io.airlift.command.args.Args1; import io.airlift.command.args.Args2; +import io.airlift.command.args.ArgsAllowedValues; import io.airlift.command.args.ArgsArityString; import io.airlift.command.args.ArgsBooleanArity; import io.airlift.command.args.ArgsBooleanArity0; @@ -215,6 +216,40 @@ public void badParameterShouldThrowParameter2Exception() { singleCommand(Args1.class).parse("-long", "foo"); } + + @Test + public void allowedValues1() + { + ArgsAllowedValues a = singleCommand(ArgsAllowedValues.class).parse("-mode", "a"); + Assert.assertEquals(a.mode, "a"); + a = singleCommand(ArgsAllowedValues.class).parse("-mode", "b"); + Assert.assertEquals(a.mode, "b"); + a = singleCommand(ArgsAllowedValues.class).parse("-mode", "c"); + Assert.assertEquals(a.mode, "c"); + } + + @Test + public void allowedValues2() + { + ArgsAllowedValues a = singleCommand(ArgsAllowedValues.class).parse("-mode=a"); + Assert.assertEquals(a.mode, "a"); + a = singleCommand(ArgsAllowedValues.class).parse("-mode=b"); + Assert.assertEquals(a.mode, "b"); + a = singleCommand(ArgsAllowedValues.class).parse("-mode=c"); + Assert.assertEquals(a.mode, "c"); + } + + @Test(expectedExceptions = ParseException.class) + public void allowedValuesShouldThrowIfNotAllowed1() + { + ArgsAllowedValues a = singleCommand(ArgsAllowedValues.class).parse("-mode", "d"); + } + + @Test(expectedExceptions = ParseException.class) + public void allowedValuesShouldThrowIfNotAllowed2() + { + ArgsAllowedValues a = singleCommand(ArgsAllowedValues.class).parse("-mode=d"); + } public void listParameters() { diff --git a/src/test/java/io/airlift/command/args/ArgsAllowedValues.java b/src/test/java/io/airlift/command/args/ArgsAllowedValues.java new file mode 100644 index 000000000..47450e5d8 --- /dev/null +++ b/src/test/java/io/airlift/command/args/ArgsAllowedValues.java @@ -0,0 +1,29 @@ +/** + * 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.args; + +import io.airlift.command.Command; +import io.airlift.command.Option; + +@Command(name="ArgsAllowedValues", description="ArgsAllowedValues description") +public class ArgsAllowedValues { + + @Option(name = "-mode", arity = 1, description = "A string from a restricted set of values", allowedValues = { "a", "b", "c" }) + public String mode; +} diff --git a/src/test/java/io/airlift/command/args/GlobalOptionsHidden.java b/src/test/java/io/airlift/command/args/GlobalOptionsHidden.java new file mode 100644 index 000000000..9737c2501 --- /dev/null +++ b/src/test/java/io/airlift/command/args/GlobalOptionsHidden.java @@ -0,0 +1,15 @@ +package io.airlift.command.args; + +import io.airlift.command.Command; +import io.airlift.command.Option; +import io.airlift.command.OptionType; + +@Command(name="GlobalOptionsHidden") +public class GlobalOptionsHidden +{ + @Option(type = OptionType.GLOBAL, name = {"-hd", "--hidden"}, hidden = true) + public boolean hiddenOption; + + @Option(type = OptionType.GLOBAL, name = {"-op" ,"--optional"}, hidden = false) + public boolean optionalOption; +} diff --git a/src/test/java/io/airlift/command/command/CommandGroupAnnotationTest.java b/src/test/java/io/airlift/command/command/CommandGroupAnnotationTest.java new file mode 100644 index 000000000..97db97ec0 --- /dev/null +++ b/src/test/java/io/airlift/command/command/CommandGroupAnnotationTest.java @@ -0,0 +1,197 @@ +package io.airlift.command.command; + +import java.util.Arrays; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import io.airlift.command.Cli; +import io.airlift.command.ParseCommandUnrecognizedException; + +public class CommandGroupAnnotationTest +{ + /* + Tests for Groups -> Group annotations + */ + @Test + public void groupIsCreatedFromGroupsAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupsAnnotation.class) + .build(); + + Object command = parser.parse("groupInsideOfGroups", "commandWithGroupsAnno", "-i", "A.java"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandWithGroupsAnnotation); + CommandWithGroupsAnnotation add = (CommandWithGroupsAnnotation) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + @Test + public void extraCommandsAreAddedFromGroupsAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupsAnnotation.class) + .build(); + + Object command = parser.parse("groupInsideOfGroups", "add", "-i", "A.java"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandAdd); + CommandAdd add = (CommandAdd) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + @Test(expectedExceptions = ParseCommandUnrecognizedException.class) + public void commandRemovedFromDefaultGroupWithGroupsAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupsAnnotation.class) + .build(); + + Object command = parser.parse("commandWithGroupsAnno", "-i", "A.java"); + + } + + /* + Note: Disabling this test for now because there's a bug when the parser parses but doesn't find a command. + It then properly uses the defaultCommand, however the input is still marked as unparsed which causes the default command to throw an exception. + We need to fix the parser/CLI to re-parse the input after calling state.withCommand(defaultCommand) + */ + @Test(enabled = false) + public void defaultCommandIsAddedFromGroupsAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupsAnnotation.class) + .build(); + + Object command = parser.parse("groupInsideOfGroups", "-i", "A.java"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandWithGroupsAnnotation); + CommandWithGroupsAnnotation add = (CommandWithGroupsAnnotation) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + /* + Tests for Group annotation + */ + @Test + public void groupIsCreatedFromGroupAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupAnnotation.class) + .build(); + + Object command = parser.parse("singleGroup", "commandWithGroup", "-i", "A.java"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandWithGroupAnnotation); + CommandWithGroupAnnotation add = (CommandWithGroupAnnotation) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + @Test + public void extraCommandsAreAddedFromGroupAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupAnnotation.class) + .build(); + + Object command = parser.parse("singleGroup", "add", "-i", "A.java"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandAdd); + CommandAdd add = (CommandAdd) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + @Test(expectedExceptions = ParseCommandUnrecognizedException.class) + public void commandRemovedFromDefaultGroupWithGroupAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupAnnotation.class) + .build(); + + Object command = parser.parse("commandWithGroup", "-i", "A.java"); + + } + + /* + Note: Disabling this test for now because there's a bug when the parser parses but doesn't find a command. + It then properly uses the defaultCommand, however the input is still marked as unparsed which causes the default command to throw an exception. + We need to fix the parser/CLI to re-parse the input after calling state.withCommand(defaultCommand) + */ + @Test(enabled = false) + public void defaultCommandIsAddedFromGroupAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupAnnotation.class) + .build(); + + Object command = parser.parse("singleGroup", "-i", "A.java"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandWithGroupAnnotation); + CommandWithGroupAnnotation add = (CommandWithGroupAnnotation) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + /* + Tests for groupNames in Command annotation + */ + @Test + public void addedToGroupFromGroupAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommands(CommandWithGroupAnnotation.class,CommandWithGroupNames.class) + .build(); + + Object command = parser.parse("singleGroup", "commandWithGroupNames", "-i", "A.java"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandWithGroupNames); + CommandWithGroupNames add = (CommandWithGroupNames) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + @Test + public void addedToSingletonGroupWithoutGroupAnnotation() + { + Cli parser = Cli + .buildCli("junk") + .withCommands(CommandWithGroupNames.class) + .build(); + + Object command = parser.parse("singletonGroup", "commandWithGroupNames", "-i", "A.java"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandWithGroupNames); + CommandWithGroupNames add = (CommandWithGroupNames) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + @Test(expectedExceptions = ParseCommandUnrecognizedException.class) + public void commandRemovedFromDefaultGroupWithGroupNames() + { + Cli parser = Cli + .buildCli("junk") + .withCommand(CommandWithGroupNames.class) + .build(); + + Object command = parser.parse("commandWithGroupNames", "-i", "A.java"); + + } + + +} diff --git a/src/test/java/io/airlift/command/command/CommandHighArityOption.java b/src/test/java/io/airlift/command/command/CommandHighArityOption.java new file mode 100644 index 000000000..bd8ee1162 --- /dev/null +++ b/src/test/java/io/airlift/command/command/CommandHighArityOption.java @@ -0,0 +1,24 @@ +package io.airlift.command.command; + +import java.util.List; + +import javax.inject.Inject; + +import io.airlift.command.Arguments; +import io.airlift.command.Command; +import io.airlift.command.Option; + +@Command(name = "cmd", description = "A command with an option that has a high arity option") +public class CommandHighArityOption { + @Inject + public CommandMain commandMain; + + @Option(name = "--option", description = "An option with high arity", arity = Integer.MAX_VALUE) + public List option; + + @Option(name = "--option2", description = "Just another option") + public String option2; + + @Arguments(description = "The rest of arguments") + public List args; +} diff --git a/src/test/java/io/airlift/command/command/CommandRemove.java b/src/test/java/io/airlift/command/command/CommandRemove.java new file mode 100644 index 000000000..7e339ce20 --- /dev/null +++ b/src/test/java/io/airlift/command/command/CommandRemove.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2013 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.command; + +import java.util.List; + +import javax.inject.Inject; + +import io.airlift.command.Arguments; +import io.airlift.command.Command; +import io.airlift.command.Option; + +/** + *

+ * + * @author Michael Grove + * @since 0 + * @version 0 + */ +@Command(name = "remove", + description = "Remove file contents to the index", + discussion = "More details about how this removes files from the index.", + examples = {"* The following is a usage example:", + "\t$ git remove -i myfile.java"}) +public class CommandRemove { + @Inject + public CommandMain commandMain; + + @Arguments(description = "Patterns of files to be added") + public List patterns; + + @Option(name = "-i") + public Boolean interactive = false; + +} diff --git a/src/test/java/io/airlift/command/command/CommandTest.java b/src/test/java/io/airlift/command/command/CommandTest.java index 5461aabd7..10d66c7c0 100644 --- a/src/test/java/io/airlift/command/command/CommandTest.java +++ b/src/test/java/io/airlift/command/command/CommandTest.java @@ -18,11 +18,14 @@ package io.airlift.command.command; +import com.google.common.collect.Lists; import io.airlift.command.Cli; +import io.airlift.command.model.CommandMetadata; import org.testng.Assert; import org.testng.annotations.Test; import java.util.Arrays; +import java.util.List; import static io.airlift.command.TestUtil.singleCommandParser; @@ -69,4 +72,90 @@ public void commandTest2() Assert.assertEquals(commit.author, "cbeust"); Assert.assertEquals(commit.files, Arrays.asList("A.java", "B.java")); } + + @Test + public void testExample() { + Cli parser = Cli.builder("git") + .withCommand(CommandRemove.class) + .build(); + + final List commandParsers = parser.getMetadata().getDefaultGroupCommands(); + + Assert.assertEquals(1, commandParsers.size()); + + CommandMetadata aMeta = commandParsers.get(0); + + Assert.assertEquals("remove", aMeta.getName()); + + Assert.assertEquals(Lists.newArrayList("* The following is a usage example:", + "\t$ git remove -i myfile.java"), aMeta.getExamples()); + } + + @Test + public void testDiscussion() { + Cli parser = Cli.builder("git") + .withCommand(CommandRemove.class) + .build(); + + final List commandParsers = parser.getMetadata().getDefaultGroupCommands(); + + Assert.assertEquals(1, commandParsers.size()); + + CommandMetadata aMeta = commandParsers.get(0); + + Assert.assertEquals("remove", aMeta.getName()); + + Assert.assertEquals("More details about how this removes files from the index.", aMeta.getDiscussion()); + } + + @Test + public void testDefaultCommandInGroup() { + Cli parser = Cli.builder("git") + .withCommand(CommandAdd.class) + .withCommand(CommandCommit.class) + .withDefaultCommand(CommandAdd.class) + .build(); + + Object command = parser.parse("-i", "A.java"); + + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandAdd); + CommandAdd add = (CommandAdd) command; + Assert.assertEquals(add.interactive.booleanValue(), true); + Assert.assertEquals(add.patterns, Arrays.asList("A.java")); + } + + @Test + public void testCommandWithArgsSeparator() { + Cli parser = Cli.builder("git") + .withCommand(CommandHighArityOption.class) + .build(); + + Object command = parser.parse("-v", "cmd", "--option", "val1", "val2", "val3", "val4", "--", "arg1", "arg2", "arg3"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandHighArityOption); + CommandHighArityOption cmdHighArity = (CommandHighArityOption) command; + + Assert.assertTrue(cmdHighArity.commandMain.verbose); + Assert.assertEquals(cmdHighArity.option, Arrays.asList("val1", "val2", "val3", "val4")); + Assert.assertEquals(cmdHighArity.args, Arrays.asList("arg1", "arg2", "arg3")); + } + + @Test + public void testCommandHighArityOptionNoSeparator() { + Cli parser = Cli.builder("git") + .withCommand(CommandHighArityOption.class) + .build(); + + // it should be able to stop parsing option values for --option if it finds another valid option (--option2) + Object command = parser.parse("-v", "cmd", "--option", "val1", "val2", "val3", "val4", "--option2", "val5", "arg1", "arg2", "arg3"); + Assert.assertNotNull(command, "command is null"); + Assert.assertTrue(command instanceof CommandHighArityOption); + CommandHighArityOption cmdHighArity = (CommandHighArityOption) command; + + Assert.assertTrue(cmdHighArity.commandMain.verbose); + Assert.assertEquals(cmdHighArity.option, Arrays.asList("val1", "val2", "val3", "val4")); + Assert.assertEquals(cmdHighArity.option2, "val5"); + Assert.assertEquals(cmdHighArity.args, Arrays.asList("arg1", "arg2", "arg3")); + } } diff --git a/src/test/java/io/airlift/command/command/CommandWithGroupAnnotation.java b/src/test/java/io/airlift/command/command/CommandWithGroupAnnotation.java new file mode 100644 index 000000000..668e91ba2 --- /dev/null +++ b/src/test/java/io/airlift/command/command/CommandWithGroupAnnotation.java @@ -0,0 +1,19 @@ +package io.airlift.command.command; + +import java.util.List; + +import io.airlift.command.Arguments; +import io.airlift.command.Command; +import io.airlift.command.Group; +import io.airlift.command.Option; + +@Group(name = "singleGroup", description = "a single group", defaultCommand = CommandWithGroupAnnotation.class,commands = {CommandAdd.class}) +@Command(name = "commandWithGroup", description = "A command with a group annotation") +public class CommandWithGroupAnnotation +{ + @Arguments(description = "Patterns of files to be added") + public List patterns; + + @Option(name = "-i") + public Boolean interactive = false; +} diff --git a/src/test/java/io/airlift/command/command/CommandWithGroupNames.java b/src/test/java/io/airlift/command/command/CommandWithGroupNames.java new file mode 100644 index 000000000..ae82df824 --- /dev/null +++ b/src/test/java/io/airlift/command/command/CommandWithGroupNames.java @@ -0,0 +1,17 @@ +package io.airlift.command.command; + +import java.util.List; + +import io.airlift.command.Arguments; +import io.airlift.command.Command; +import io.airlift.command.Option; + +@Command(name = "commandWithGroupNames", description = "A command with a group annotation", groupNames = {"singleGroup","singletonGroup"}) +public class CommandWithGroupNames +{ + @Arguments(description = "Patterns of files to be added") + public List patterns; + + @Option(name = "-i") + public Boolean interactive = false; +} diff --git a/src/test/java/io/airlift/command/command/CommandWithGroupsAnnotation.java b/src/test/java/io/airlift/command/command/CommandWithGroupsAnnotation.java new file mode 100644 index 000000000..791779f0f --- /dev/null +++ b/src/test/java/io/airlift/command/command/CommandWithGroupsAnnotation.java @@ -0,0 +1,19 @@ +package io.airlift.command.command; + +import java.util.List; + +import io.airlift.command.*; + +@Groups({ + @Group(name = "groupInsideOfGroups", description = "my nested group", defaultCommand = CommandWithGroupsAnnotation.class,commands = {CommandAdd.class}) +}) +@Command(name = "commandWithGroupsAnno", description = "A command with a groups annotation") +public class CommandWithGroupsAnnotation +{ + @Arguments(description = "Patterns of files to be added") + public List patterns; + + @Option(name = "-i") + public Boolean interactive = false; + +}