/* Copyright 2013 Jonatan Jönsson
*
* 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 se.softhouse.jargo;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Arrays.asList;
import static se.softhouse.jargo.Arguments.command;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import se.softhouse.common.strings.Describable;
import se.softhouse.jargo.ArgumentBuilder.SimpleArgumentBuilder;
import se.softhouse.jargo.StringParsers.RunnableParser;
import com.google.common.collect.Lists;
/**
* Manages multiple {@link Argument}s and/or {@link Command}s. The brain of this API.
* Its primary goal is to decide which {@link Argument} each input string belongs to.
* A {@link CommandLineParser} can be reused to parse arguments over and over again (even
* concurrently). Different {@link CommandLineParser}s can even share {@link Argument}
* configurations as {@link Argument} instances are immutable as well. Documentation through
* example:
*
* <pre class="prettyprint">
* <code class="language-java">
* import static se.softhouse.jargo.Arguments.*;
* ...
* String[] args = {"--enable-logging", "--listen-port", "8090", "Hello"};
*
* Argument<?> helpArgument = helpArgument("-h", "--help"); //Will throw when -h is encountered
* Argument<Boolean> enableLogging = optionArgument("-l", "--enable-logging").description("Output debug information to standard out").build();
* Argument<String> greetingPhrase = stringArgument().description("A greeting phrase to greet new connections with").build();
* Argument<List<Integer>> ports = integerArgument("-p", "--listen-port")
* .defaultValue(8080)
* .description("The port clients should connect to.")
* .metaDescription("<port>")
* .limitTo(Range.closed(0, 65536))
* .repeated().build();
*
* try
* {
* ParsedArguments arguments = CommandLineParser.withArguments(helpArgument, greetingPhrase, enableLogging, ports).parse(args);
* assertThat(arguments.get(enableLogging)).isTrue();
* assertThat(arguments.get(ports)).isEqualTo(Arrays.asList(8090));
* assertThat(arguments.get(greetingPhrase)).isEqualTo("Hello");
* }
* catch(ArgumentException exception)
* {
* System.out.println(exception.getMessageAndUsage());
* System.exit(1);
* }
* </code>
* </pre>
*
* <pre>
* For this program the usage would look like ("YourProgramName" is fetched from stack traces by default):
* <code>
* Usage: YourProgramName [Arguments]
*
* Arguments:
* <string> A greeting phrase to greet new connections with
* <string>: any string
* Default:
* -l, --enable-logging Output debug information to standard out
* Default: disabled
* -p, --listen-port <port> The port clients should connect to [Supports Multiple occurrences]
* port: [0‥65536]
* Default: 8080
* </code>
* If something goes wrong during the parsing (Missing required arguments, Unexpected arguments, Invalid values),
* it will be described by the ArgumentException. Use {@link ArgumentException#getMessageAndUsage()} if you
* want to explain what went wrong to the user.
* </pre>
*
* <b>Internationalization</b> By default {@link Locale#US} is used for parsing strings and printing
* usages. To change this use {@link #locale(Locale)}.<br>
* <b>Thread safety concerns:</b> If there is a parsing occurring while any modifying method is
* invoked, such as {@link #programName(String)}, the changes won't be seen until the start of the
* next parse invocation. Also, if the goal is to set {@link #programName(String)} and
* {@link #programDescription(String)} atomically you'll have to synchronize such changes
* externally.
*/
@ThreadSafe
public final class CommandLineParser
{
// Internally CommandLineParser is a builder for CommandLineParserInstance but the idea is to
// keep this idea hidden in the API to lessen the API complexity
static final Locale US_BY_DEFAULT = Locale.US;
@GuardedBy("modifyGuard") private volatile CommandLineParserInstance cachedParser;
/**
* Use of this lock makes sure that there's no race condition if several concurrent calls
* to addArguments are made at the same time, in such a race some argument definitions
* could be "lost" without this lock.
*/
private final Lock modifyGuard = new ReentrantLock();
private CommandLineParser(Iterable<Argument<?>> argumentDefinitions)
{
this.cachedParser = new CommandLineParserInstance(argumentDefinitions);
}
CommandLineParserInstance parser()
{
return cachedParser;
}
/**
* Creates a {@link CommandLineParser} with support for the given {@code argumentDefinitions}.
* {@link Command}s can be added later with {@link #andCommands(Command...)}.
*
* @param argumentDefinitions {@link Argument}s produced with {@link Arguments} or
* with your own disciples of {@link ArgumentBuilder}
* @return a CommandLineParser which you can call {@link CommandLineParser#parse(String...)} on
* and get {@link ParsedArguments} out of.
* @throws IllegalArgumentException if the given {@code argumentDefinitions} would create an
* erroneous CommandLineParser
* @see CommandLineParser
*/
@CheckReturnValue
@Nonnull
public static CommandLineParser withArguments(final Argument<?> ... argumentDefinitions)
{
return new CommandLineParser(Arrays.asList(argumentDefinitions));
}
/**
* {@link Iterable} version of {@link #withArguments(Argument...)}
*/
@CheckReturnValue
@Nonnull
public static CommandLineParser withArguments(final Iterable<Argument<?>> argumentDefinitions)
{
return new CommandLineParser(argumentDefinitions);
}
/**
* Creates a {@link CommandLineParser} with support for {@code commands}. To add additional
* {@link Argument}s or {@link Command}s there is {@link #andArguments(Argument...)} and
* {@link #andCommands(Command...)}.
*
* @param commands the commands to support initially
* @return a CommandLineParser which you can call {@link CommandLineParser#parse(String...)} on
* and get {@link ParsedArguments} out of.
* @see CommandLineParser
*/
@CheckReturnValue
@Nonnull
public static CommandLineParser withCommands(final Command ... commands)
{
return new CommandLineParser(commandsToArguments(commands));
}
/**
* An alternative to {@link Command} that accepts an {@link Enum} following the <a
* href="http://en.wikipedia.org/wiki/Command_pattern">Command
* Pattern</a>. This alternative is viable if the commands don't accept any parameters.
* Example enum:
*
* <pre class="prettyprint">
* <code class="language-java">
* public enum Service implements Runnable, Describable
* {
* START{
* @Override
* public void run(){
* //Start service here
* }
*
* @Override
* public String description(){
* return "Starts the service";
* }
* };
* }
* </code>
* </pre>
*
* The {@link ArgumentBuilder#names(String...) name} for each command will be the enum constants
* {@link Enum#name() name} in {@link String#toLowerCase(Locale) lower case}.
*
* @param commandEnum the {@link Class} <i>literal</i> for {@code Service} in the example above
* @return a CommandLineParser which you can call {@link CommandLineParser#parse(String...)} on
*/
public static <E extends Enum<E> & Runnable & Describable> CommandLineParser withCommands(Class<E> commandEnum)
{
return new CommandLineParser(commandsToArguments(commandEnum));
}
/**
* Parses {@code actualArguments} (typically from the command line, i.e argv) and returns the
* parsed values in a {@link ParsedArguments} container. {@link Locale#US} is used to parse
* strings by default. Use {@link #locale(Locale)} to change it.
*
* @throws ArgumentException if an invalid argument is encountered during the parsing
*/
@Nonnull
public ParsedArguments parse(final String ... actualArguments) throws ArgumentException
{
return parser().parse(asList(actualArguments));
}
/**
* {@link Iterable} version of {@link #parse(String...)}
*/
@Nonnull
public ParsedArguments parse(final Iterable<String> actualArguments) throws ArgumentException
{
return parser().parse(actualArguments);
}
/**
* Returns a usage text describing all arguments this {@link CommandLineParser} handles.
* Suitable to print on {@link System#out}.
*/
@CheckReturnValue
@Nonnull
public Usage usage()
{
return new Usage(parser());
}
/**
* Adds support for {@code commandsToAlsoSupport} in this {@link CommandLineParser}. Typically
* used in a chained fashion when faced with many supported {@link Command}s.
*
* @param commandsToAlsoSupport the commands to add support for
* @return this {@link CommandLineParser} to allow for chained calls
*/
@CheckReturnValue
@Nonnull
public CommandLineParser andCommands(final Command ... commandsToAlsoSupport)
{
verifiedAdd(commandsToArguments(commandsToAlsoSupport));
return this;
}
/**
* <pre>
* Adds support for {@code argumentsToAlsoSupport} in this {@link CommandLineParser}.
* Typically used in a chained fashion when faced with many supported arguments.
* Another usage is simply for readability: group arguments by logical groups
*
* @param argumentsToAlsoSupport the arguments to add support for
* @return this {@link CommandLineParser} to allow for chained calls
* </pre>
*/
@CheckReturnValue
@Nonnull
public CommandLineParser andArguments(final Argument<?> ... argumentsToAlsoSupport)
{
verifiedAdd(asList(argumentsToAlsoSupport));
return this;
}
/**
* Verify that the parser would be usable after adding {@code argumentsToAdd} to it.
* This allows future parse operations to still use the unaffected old parser.
*/
private void verifiedAdd(Collection<Argument<?>> argumentsToAdd)
{
try
{
modifyGuard.lock();
List<Argument<?>> newDefinitions = Lists.newArrayList(parser().allArguments());
newDefinitions.addAll(argumentsToAdd);
cachedParser = new CommandLineParserInstance(newDefinitions, parser().programInformation(), parser().locale(), false);
}
finally
{
modifyGuard.unlock();
}
}
/**
* Sets the {@code programName} to print with {@link #usage()}
*
* @return this parser
*/
public CommandLineParser programName(String programName)
{
try
{
modifyGuard.lock();
ProgramInformation programInformation = parser().programInformation().programName(programName);
cachedParser = new CommandLineParserInstance(parser().allArguments(), programInformation, parser().locale(), false);
}
finally
{
modifyGuard.unlock();
}
return this;
}
/**
* Sets the {@code programDescription} to print with {@link #usage()}
*
* @return this parser
*/
public CommandLineParser programDescription(String programDescription)
{
try
{
modifyGuard.lock();
ProgramInformation programInformation = parser().programInformation().programDescription(programDescription);
cachedParser = new CommandLineParserInstance(parser().allArguments(), programInformation, parser().locale(), false);
}
finally
{
modifyGuard.unlock();
}
return this;
}
/**
* {@link Locale#getDefault()} is dangerous to use. It's a global and mutable variable
* which makes it hard to give any guarantees on what it will be at a certain time. This is why
* {@link Locale#US} is used by default instead.
* If {@link Locale#getDefault()} is wanted, use {@link #locale(Locale)
* locale(Locale.getDefault())}.
*
* @param localeToUse the {@link Locale} to parse input strings with (it will be passed to
* {@link StringParser#parse(String, Locale)} and
* {@link StringParser#descriptionOfValidValues(Locale)})
* @return this parser
*/
public CommandLineParser locale(Locale localeToUse)
{
try
{
modifyGuard.lock();
cachedParser = new CommandLineParserInstance(parser().allArguments(), parser().programInformation(), checkNotNull(localeToUse), false);
}
finally
{
modifyGuard.unlock();
}
return this;
}
/**
* Returns the {@link #usage()} for this {@link CommandLineParser}
*/
@Override
public String toString()
{
return usage().toString();
}
private static List<Argument<?>> commandsToArguments(final Command ... commands)
{
List<Argument<?>> commandsAsArguments = Lists.newArrayListWithExpectedSize(commands.length);
for(Command c : commands)
{
commandsAsArguments.add(command(c).build());
}
return commandsAsArguments;
}
private static <E extends Enum<E> & Runnable & Describable> List<Argument<?>> commandsToArguments(Class<E> commandEnum)
{
List<Argument<?>> commandsAsArguments = Lists.newArrayList();
for(E command : commandEnum.getEnumConstants())
{
Argument<Object> commandAsArgument = new SimpleArgumentBuilder<Object>(new RunnableParser(command)) //
.names(command.name().toLowerCase(Locale.US)) //
.description(command) //
.build();
commandsAsArguments.add(commandAsArgument);
}
return commandsAsArguments;
}
}