/*
* Copyright (C) 2014 Facebook, Inc.
*
* 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 com.facebook.tools.parser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Specifies valid command usage. See {@link com.facebook.tools.parser.CliCommand.Builder} for more
* details.
*/
public class CliCommand {
private final String name;
private final List<String> description;
private final List<String> notes;
private final List<CliOption> options;
private final List<CliParameter> parameters;
private final boolean allowsTrailingParameters;
private CliCommand(
String name,
List<String> description,
List<String> notes,
List<CliOption> options,
List<CliParameter> parameters,
boolean allowsTrailingParameters
) {
this.name = name;
this.description = new ArrayList<>(description);
this.notes = new ArrayList<>(notes);
this.options = new ArrayList<>(options);
this.parameters = new ArrayList<>(parameters);
this.allowsTrailingParameters = allowsTrailingParameters;
}
public String getName() {
return name;
}
public List<String> getDescription() {
return description;
}
public List<String> getNotes() {
return notes;
}
public List<CliOption> getOptions() {
return options;
}
public List<CliParameter> getParameters() {
return parameters;
}
public boolean allowsTrailingParameter() {
return allowsTrailingParameters;
}
public String getDocumentation() {
StringBuilder result = new StringBuilder(80);
appendDocumentation(result);
return result.toString();
}
@Override
public String toString() {
return "CliCommandDefinition{" +
"name='" + name + '\'' +
'}';
}
public static class Builder {
private final String name;
private final List<String> description;
private final List<CliOption.Builder> options = new ArrayList<>();
private final List<CliParameter.Builder> parameters = new ArrayList<>();
private List<String> notes = Collections.emptyList();
private boolean allowsTrailingParameters;
/**
* Defines the command name and general description.
*
* @param name the name of the command
* @param description the description displayed when printing usage help
* @param additionalLines syntactic sugar for multi-line descriptions
*/
public Builder(String name, String description, String... additionalLines) {
this.name = name;
this.description = new ArrayList<>();
this.description.add(description);
this.description.addAll(Arrays.asList(additionalLines));
}
/**
* Adds additional documentation displayed after the main description and argument docs.
*
* @param notes
* @param additionalLines
* @return this builder
*/
public Builder withNotes(String notes, String... additionalLines) {
this.notes = new ArrayList<>();
this.notes.add(notes);
this.notes.addAll(Arrays.asList(additionalLines));
return this;
}
/**
* Adds a named option that takes a parameter, e.g., {@code --input foo.txt}.
*
* @param switchName the name, including any dashes, e.g., {@code --input}
* @param additionaSwitchNames synonyms for the name, e.g., {@code -i}
* @return this builder
*/
public CliOption.SwitchBuilder addOption(String switchName, String... additionaSwitchNames) {
CliOption.SwitchBuilder builder = new CliOption.SwitchBuilder();
builder.withSwitch(switchName);
builder.withSwitch(additionaSwitchNames);
options.add(builder);
return builder;
}
/**
* Adds a named option that doesn't take a parameter, e.g., {@code --debug}.
*
* @param switchName the name, including any dashes, e.g., {@code --debug}
* @param additionaSwitchNames synonyms for the name, e.g., {@code -d}
* @return this builder
*/
public CliOption.FlagBuilder addFlag(String switchName, String... additionaSwitchNames) {
CliOption.FlagBuilder builder = new CliOption.FlagBuilder();
builder.withSwitch(switchName);
builder.withSwitch(additionaSwitchNames);
options.add(builder);
return builder;
}
/**
* Adds a positional parameter. For example:
* <code>
* CliCommand.Builder builder = new CliCommand.Builder("cat", "Prints the contents of a file");
* builder.addParameter("file").withDescription("The file to print")
* </code>
*
* @param name the name used to refer to the parameter as this position
* @return this builder
*/
public CliParameter.Builder addParameter(String name) {
CliParameter.Builder builder = CliParameter.Builder.withName(name);
parameters.add(builder);
return builder;
}
public Builder allowTrailingParameters() {
this.allowsTrailingParameters = true;
return this;
}
public CliCommand build() {
List<CliOption> options = new ArrayList<>();
Set<String> names = new HashSet<>(this.options.size());
for (CliOption.Builder builder : this.options) {
CliOption option = builder.build();
for (String switchName : option.getSwitchNames()) {
if (!names.add(switchName)) {
throw new IllegalStateException("Switch name collision: " + switchName);
}
}
options.add(option);
}
List<CliParameter> parameters = new ArrayList<>(this.parameters.size());
for (CliParameter.Builder builder : this.parameters) {
CliParameter parameter = builder.build();
if (!names.add(parameter.getName())) {
throw new IllegalStateException("Parameter name collision: " + parameter.getName());
}
parameters.add(parameter);
}
return new CliCommand(
name, description, notes, options, parameters, allowsTrailingParameters
);
}
}
private void appendDocumentation(StringBuilder result) {
result.append(getName());
for (CliParameter parameter : getParameters()) {
result.append(" <").append(parameter.getName()).append(">");
}
for (String line : getDescription()) {
result.append('\n').append(" ").append(line);
}
if (!getOptions().isEmpty()) {
result.append('\n');
for (CliOption option : getOptions()) {
result.append('\n');
appendDocumentation(result, option);
}
}
if (!getNotes().isEmpty()) {
result.append('\n');
for (String note : getNotes()) {
result.append('\n').append(" ").append(note);
}
}
}
private void appendDocumentation(StringBuilder result, CliOption option) {
Iterator<String> switchNames = option.getSwitchNames().iterator();
result.append(" ");
while (switchNames.hasNext()) {
result.append(switchNames.next());
if (switchNames.hasNext()) {
result.append(' ');
}
}
if (!option.isFlag()) {
result.append(" <").append(option.getMetavar()).append('>');
}
result.append('\n');
result.append(" [").append(option.isRequired() ? "Required" : "Optional").append(']');
Iterator<String> descriptionIterator = option.getDescription().iterator();
if (descriptionIterator.hasNext()) {
result.append(" ");
while (descriptionIterator.hasNext()) {
result.append(descriptionIterator.next());
if (descriptionIterator.hasNext()) {
result.append("\n").append(" ");
}
}
}
for (String example : option.getExamples()) {
result.append("\n").append(" e.g., ").append(example);
}
if (option.getDefaultValue() != null && !option.isFlag()) {
result.append("\n").append(" default: ").append(option.getDefaultValue());
}
}
}