Added a cli infrastructure

CliTool is a base class for command-line interface tools (such as the plugin manager and potentially others). It supports the following:
  - single or multi command tool
  - help printing infrastructure (based on help files)
  - consistent mechanism of parsing arguments (based on commons-cli lib)
  - separation of argument parsing and command execution (for easier unit testing)
  - terminal abstraction (will use System.console() when available)
This commit is contained in:
uboness 2014-08-02 17:16:27 +02:00
parent 2d31349ab0
commit 5ccc7beaf4
13 changed files with 1362 additions and 7 deletions

15
pom.xml
View File

@ -263,6 +263,13 @@
<version>3.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.2</version>
</dependency>
<!-- END: dependencies that are shaded -->
<dependency>
@ -667,6 +674,7 @@
<include>com.ning:compress-lzf</include>
<include>com.github.spullara.mustache.java:compiler</include>
<include>com.tdunning:t-digest</include>
<include>commons-cli:commons-cli</include>
</includes>
</artifactSet>
<relocations>
@ -706,6 +714,10 @@
<pattern>com.tdunning.math.stats</pattern>
<shadedPattern>org.elasticsearch.common.stats</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons.cli</pattern>
<shadedPattern>org.elasticsearch.common.cli.commons</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
@ -1153,7 +1165,10 @@
<exclude>jsr166e/**</exclude>
<!-- start excludes for valid system-out -->
<exclude>org/elasticsearch/common/logging/log4j/ConsoleAppender*</exclude>
<exclude>org/elasticsearch/common/http/client/HttpDownloadHelper*</exclude>
<exclude>org/elasticsearch/common/cli/Terminal*</exclude>
<exclude>org/elasticsearch/plugins/PluginManager.class</exclude>
<exclude>org/elasticsearch/common/http/client/HttpDownloadHelper.class</exclude>
<exclude>org/elasticsearch/bootstrap/Bootstrap.class</exclude>
<exclude>org/elasticsearch/Version.class</exclude>
<exclude>org/elasticsearch/common/lucene/search/Queries$QueryWrapperFilterFactory.class</exclude>

View File

@ -0,0 +1,249 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.cli;
import com.google.common.base.Preconditions;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import java.io.IOException;
import java.util.Locale;
import static org.elasticsearch.common.settings.ImmutableSettings.Builder.EMPTY_SETTINGS;
/**
* A base class for command-line interface tool.
*
* Two modes are supported:
*
* - Single command mode. The tool exposes a single command that can potentially accept arguments (eg. CLI options).
* - Multi command mode. The tool support multiple commands, each for different tasks, each potentially accepts arguments.
*
* In a multi-command mode. The first argument must be the command name. For example, the plugin manager
* can be seen as a multi-command tool with two possible commands: install and uninstall
*
* The tool is configured using a {@link CliToolConfig} which encapsulates the tool's commands and their
* potential options. The tool also comes with out of the box simple help support (the -h/--help option is
* automatically handled) where the help text is configured in a dedicated *.help files located in the same package
* as the tool.
*/
public abstract class CliTool {
// based on sysexits.h
public static enum ExitStatus {
OK(0),
USAGE(64), /* command line usage error */
DATA_ERROR(65), /* data format error */
NO_INPUT(66), /* cannot open input */
NO_USER(67), /* addressee unknown */
NO_HOST(68), /* host name unknown */
UNAVAILABLE(69), /* service unavailable */
CODE_ERROR(70), /* internal software error */
CANT_CREATE(73), /* can't create (user) output file */
IO_ERROR(74), /* input/output error */
TEMP_FAILURE(75), /* temp failure; user is invited to retry */
PROTOCOL(76), /* remote error in protocol */
NOPERM(77), /* permission denied */
CONFIG(78); /* configuration error */
final int status;
private ExitStatus(int status) {
this.status = status;
}
public int status() {
return status;
}
}
protected final Terminal terminal;
protected final Environment env;
protected final Settings settings;
private final CliToolConfig config;
protected CliTool(CliToolConfig config) {
this(config, Terminal.DEFAULT);
}
protected CliTool(CliToolConfig config, Terminal terminal) {
Preconditions.checkArgument(config.cmds().size() != 0, "At least one command must be configured");
this.config = config;
this.terminal = terminal;
Tuple<Settings, Environment> tuple = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true);
settings = tuple.v1();
env = tuple.v2();
}
public final int execute(String... args) {
// first lets see if the user requests tool help. We're doing it only if
// this is a multi-command tool. If it's a single command tool, the -h/--help
// option will be taken care of on the command level
if (!config.isSingle() && args.length > 0 && (args[0].equals("-h") || args[0].equals("--help"))) {
config.printUsage(terminal);
return ExitStatus.OK.status;
}
CliToolConfig.Cmd cmd;
if (config.isSingle()) {
cmd = config.single();
} else {
if (args.length == 0) {
terminal.printError("command not specified");
config.printUsage(terminal);
return ExitStatus.USAGE.status;
}
String cmdName = args[0];
cmd = config.cmd(cmdName);
if (cmd == null) {
terminal.printError("unknown command [%s]. Use [-h] option to list available commands", cmdName);
return ExitStatus.USAGE.status;
}
// we now remove the command name from the args
if (args.length == 1) {
args = new String[0];
} else {
String[] cmdArgs = new String[args.length - 1];
System.arraycopy(args, 1, cmdArgs, 0, cmdArgs.length);
args = cmdArgs;
}
}
Command command = null;
try {
command = parse(cmd, args);
return command.execute(settings, env).status;
} catch (IOException ioe) {
terminal.printError(ioe.getMessage());
return ExitStatus.IO_ERROR.status;
} catch (IllegalArgumentException | ElasticsearchIllegalArgumentException ilae) {
terminal.printError(ilae.getMessage());
return ExitStatus.USAGE.status;
} catch (Throwable t) {
terminal.printError(t.getMessage());
if (command == null) {
return ExitStatus.USAGE.status;
}
return ExitStatus.CODE_ERROR.status;
}
}
public Command parse(String cmdName, String[] args) throws Exception {
CliToolConfig.Cmd cmd = config.cmd(cmdName);
return parse(cmd, args);
}
public Command parse(CliToolConfig.Cmd cmd, String[] args) throws Exception {
CommandLineParser parser = new GnuParser();
CommandLine cli = parser.parse(CliToolConfig.OptionsSource.HELP.options(), args, true);
if (cli.hasOption("h")) {
return helpCmd(cmd);
}
cli = parser.parse(cmd.options(), args);
Terminal.Verbosity verbosity = Terminal.Verbosity.resolve(cli);
terminal.verbosity(verbosity);
return parse(cmd.name(), cli);
}
protected Command.Help helpCmd(CliToolConfig.Cmd cmd) {
return new Command.Help(cmd, terminal);
}
protected static Command.Exit exitCmd(ExitStatus status) {
return new Command.Exit(null, status, null);
}
protected static Command.Exit exitCmd(ExitStatus status, Terminal terminal, String msg, Object... args) {
return new Command.Exit(String.format(Locale.ROOT, msg, args), status, terminal);
}
protected abstract Command parse(String cmdName, CommandLine cli) throws Exception;
public static abstract class Command {
protected final Terminal terminal;
protected Command(Terminal terminal) {
this.terminal = terminal;
}
public abstract ExitStatus execute(Settings settings, Environment env) throws Exception;
public static class Help extends Command {
private final CliToolConfig.Cmd cmd;
private Help(CliToolConfig.Cmd cmd, Terminal terminal) {
super(terminal);
this.cmd = cmd;
}
@Override
public ExitStatus execute(Settings settings, Environment env) throws Exception {
cmd.printUsage(terminal);
return ExitStatus.OK;
}
}
public static class Exit extends Command {
private final String msg;
private final ExitStatus status;
private Exit(String msg, ExitStatus status, Terminal terminal) {
super(terminal);
this.msg = msg;
this.status = status;
}
@Override
public ExitStatus execute(Settings settings, Environment env) throws Exception {
if (msg != null) {
if (status != ExitStatus.OK) {
terminal.printError(msg);
} else {
terminal.println(msg);
}
}
return status;
}
public ExitStatus status() {
return status;
}
}
}
}

View File

@ -0,0 +1,271 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.cli;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import java.util.Collection;
/**
*
*/
public class CliToolConfig {
public static Builder config(String name, Class<? extends CliTool> toolType) {
return new Builder(name, toolType);
}
private final Class<? extends CliTool> toolType;
private final String name;
private final ImmutableMap<String, Cmd> cmds;
private static final HelpPrinter helpPrinter = new HelpPrinter();
private CliToolConfig(String name, Class<? extends CliTool> toolType, Cmd[] cmds) {
this.name = name;
this.toolType = toolType;
ImmutableMap.Builder<String, Cmd> cmdsBuilder = ImmutableMap.builder();
for (int i = 0; i < cmds.length; i++) {
cmdsBuilder.put(cmds[i].name, cmds[i]);
}
this.cmds = cmdsBuilder.build();
}
public boolean isSingle() {
return cmds.size() == 1;
}
public Cmd single() {
assert isSingle() : "Requesting single command on a multi-command tool";
return cmds.values().iterator().next();
}
public Class<? extends CliTool> toolType() {
return toolType;
}
public String name() {
return name;
}
public Collection<Cmd> cmds() {
return cmds.values();
}
public Cmd cmd(String name) {
return cmds.get(name);
}
public void printUsage(Terminal terminal) {
helpPrinter.print(this, terminal);
}
public static class Builder {
public static Cmd.Builder cmd(String name, Class<? extends CliTool.Command> cmdType) {
return new Cmd.Builder(name, cmdType);
}
public static OptionBuilder option(String shortName, String longName) {
return new OptionBuilder(shortName, longName);
}
public static OptionGroupBuilder optionGroup(boolean required) {
return new OptionGroupBuilder(required);
}
private final Class<? extends CliTool> toolType;
private final String name;
private Cmd[] cmds;
private Builder(String name, Class<? extends CliTool> toolType) {
this.name = name;
this.toolType = toolType;
}
public Builder cmds(Cmd.Builder... cmds) {
this.cmds = new Cmd[cmds.length];
for (int i = 0; i < cmds.length; i++) {
this.cmds[i] = cmds[i].build();
this.cmds[i].toolName = name;
}
return this;
}
public Builder cmds(Cmd... cmds) {
for (int i = 0; i < cmds.length; i++) {
cmds[i].toolName = name;
}
this.cmds = cmds;
return this;
}
public CliToolConfig build() {
return new CliToolConfig(name, toolType, cmds);
}
}
public static class Cmd {
private String toolName;
private final String name;
private final Class<? extends CliTool.Command> cmdType;
private final Options options;
private Cmd(String name, Class<? extends CliTool.Command> cmdType, Options options) {
this.name = name;
this.cmdType = cmdType;
this.options = options;
OptionsSource.VERBOSITY.populate(options);
}
public Class<? extends CliTool.Command> cmdType() {
return cmdType;
}
public String name() {
return name;
}
public Options options() {
return options;
}
public void printUsage(Terminal terminal) {
helpPrinter.print(toolName, this, terminal);
}
public static class Builder {
private final String name;
private final Class<? extends CliTool.Command> cmdType;
private Options options = new Options();
private Builder(String name, Class<? extends CliTool.Command> cmdType) {
this.name = name;
this.cmdType = cmdType;
}
public Builder options(OptionBuilder... optionBuilder) {
for (int i = 0; i < optionBuilder.length; i++) {
options.addOption(optionBuilder[i].build());
}
return this;
}
public Builder optionGroups(OptionGroupBuilder... optionGroupBuilders) {
for (OptionGroupBuilder builder : optionGroupBuilders) {
options.addOptionGroup(builder.build());
}
return this;
}
public Cmd build() {
return new Cmd(name, cmdType, options);
}
}
}
public static class OptionBuilder {
private final Option option;
private OptionBuilder(String shortName, String longName) {
option = new Option(shortName, "");
option.setLongOpt(longName);
option.setArgName(longName);
}
public OptionBuilder required(boolean required) {
option.setRequired(required);
return this;
}
public OptionBuilder hasArg(boolean optional) {
option.setOptionalArg(optional);
option.setArgs(1);
return this;
}
public Option build() {
return option;
}
}
public static class OptionGroupBuilder {
private OptionGroup group;
private OptionGroupBuilder(boolean required) {
group = new OptionGroup();
group.setRequired(required);
}
public OptionGroupBuilder options(OptionBuilder... optionBuilders) {
for (OptionBuilder builder : optionBuilders) {
group.addOption(builder.build());
}
return this;
}
public OptionGroup build() {
return group;
}
}
static abstract class OptionsSource {
static final OptionsSource HELP = new OptionsSource() {
@Override
void populate(Options options) {
options.addOption(new OptionBuilder("h", "help").required(false).build());
}
};
static final OptionsSource VERBOSITY = new OptionsSource() {
@Override
void populate(Options options) {
OptionGroup verbosityGroup = new OptionGroup();
verbosityGroup.setRequired(false);
verbosityGroup.addOption(new OptionBuilder("s", "silent").required(false).build());
verbosityGroup.addOption(new OptionBuilder("v", "verbose").required(false).build());
options.addOptionGroup(verbosityGroup);
}
};
private Options options;
Options options() {
if (options == null) {
options = new Options();
populate(options);
}
return options;
}
abstract void populate(Options options);
}
}

View File

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.cli;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.util.Callback;
import java.io.IOException;
import java.io.InputStream;
/**
*
*/
public class HelpPrinter {
private static final String HELP_FILE_EXT = ".help";
public void print(CliToolConfig config, Terminal terminal) {
print(config.toolType(), config.name(), terminal);
}
public void print(String toolName, CliToolConfig.Cmd cmd, Terminal terminal) {
print(cmd.cmdType(), toolName + "-" + cmd.name(), terminal);
}
private static void print(Class clazz, String name, final Terminal terminal) {
terminal.println(Terminal.Verbosity.SILENT);
try (InputStream input = clazz.getResourceAsStream(name + HELP_FILE_EXT)) {
Streams.readAllLines(input, new Callback<String>() {
@Override
public void handle(String line) {
terminal.println(Terminal.Verbosity.SILENT, line);
}
});
} catch (IOException ioe) {
ioe.printStackTrace(terminal.writer());
}
terminal.println();
}
}

View File

@ -0,0 +1,176 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.cli;
import org.apache.commons.cli.CommandLine;
import java.io.*;
import java.util.Locale;
/**
*
*/
public abstract class Terminal {
public static final Terminal DEFAULT = ConsoleTerminal.supported() ? new ConsoleTerminal() : new SystemTerminal();
public static enum Verbosity {
SILENT(0), NORMAL(1), VERBOSE(2);
private final int level;
private Verbosity(int level) {
this.level = level;
}
public boolean enabled(Verbosity verbosity) {
return level >= verbosity.level;
}
public static Verbosity resolve(CommandLine cli) {
if (cli.hasOption("s")) {
return SILENT;
}
if (cli.hasOption("v")) {
return VERBOSE;
}
return NORMAL;
}
}
private Verbosity verbosity = Verbosity.NORMAL;
public Terminal() {
this(Verbosity.NORMAL);
}
public Terminal(Verbosity verbosity) {
this.verbosity = verbosity;
}
public void verbosity(Verbosity verbosity) {
this.verbosity = verbosity;
}
public Verbosity verbosity() {
return verbosity;
}
public abstract String readText(String text, Object... args);
public abstract char[] readSecret(String text, Object... args);
public void println() {
println(Verbosity.NORMAL);
}
public void println(String msg, Object... args) {
println(Verbosity.NORMAL, msg, args);
}
public void print(String msg, Object... args) {
print(Verbosity.NORMAL, msg, args);
}
public void println(Verbosity verbosity) {
println(verbosity, "");
}
public void println(Verbosity verbosity, String msg, Object... args) {
print(verbosity, msg + System.lineSeparator(), args);
}
public void print(Verbosity verbosity, String msg, Object... args) {
if (this.verbosity.enabled(verbosity)) {
doPrint(msg, args);
}
}
public void printError(String msg, Object... args) {
println(Verbosity.SILENT, "ERROR: " + msg, args);
}
protected abstract void doPrint(String msg, Object... args);
public abstract PrintWriter writer();
private static class ConsoleTerminal extends Terminal {
final Console console = System.console();
static boolean supported() {
return System.console() != null;
}
@Override
public void doPrint(String msg, Object... args) {
console.printf(msg, args);
console.flush();
}
@Override
public String readText(String text, Object... args) {
return console.readLine(text, args);
}
@Override
public char[] readSecret(String text, Object... args) {
return console.readPassword(text, args);
}
@Override
public PrintWriter writer() {
return console.writer();
}
}
private static class SystemTerminal extends Terminal {
private final PrintWriter printWriter = new PrintWriter(System.out);
@Override
public void doPrint(String msg, Object... args) {
System.out.print(String.format(Locale.ROOT, msg, args));
}
@Override
public String readText(String text, Object... args) {
print(text, args);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
return reader.readLine();
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
@Override
public char[] readSecret(String text, Object... args) {
return readText(text, args).toCharArray();
}
@Override
public PrintWriter writer() {
return printWriter;
}
}
}

View File

@ -130,7 +130,7 @@ public class HttpDownloadHelper {
public static class VerboseProgress implements DownloadProgress {
private int dots = 0;
// CheckStyle:VisibilityModifier OFF - bc
PrintStream out;
PrintWriter writer;
// CheckStyle:VisibilityModifier ON
/**
@ -139,14 +139,23 @@ public class HttpDownloadHelper {
* @param out the output stream.
*/
public VerboseProgress(PrintStream out) {
this.out = out;
this.writer = new PrintWriter(out);
}
/**
* Construct a verbose progress reporter.
*
* @param writer the output stream.
*/
public VerboseProgress(PrintWriter writer) {
this.writer = this.writer;
}
/**
* begin a download
*/
public void beginDownload() {
out.print("Downloading ");
writer.print("Downloading ");
dots = 0;
}
@ -154,9 +163,9 @@ public class HttpDownloadHelper {
* tick handler
*/
public void onTick() {
out.print(".");
writer.print(".");
if (dots++ > 50) {
out.flush();
writer.flush();
dots = 0;
}
}
@ -165,8 +174,8 @@ public class HttpDownloadHelper {
* end a download
*/
public void endDownload() {
out.println("DONE");
out.flush();
writer.println("DONE");
writer.flush();
}
}

View File

@ -20,10 +20,13 @@
package org.elasticsearch.common.io;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import org.elasticsearch.common.Preconditions;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.util.Callback;
import java.io.*;
import java.util.List;
/**
* Simple utility methods for file and stream copying.
@ -297,4 +300,24 @@ public abstract class Streams {
}
return read;
}
public static List<String> readAllLines(InputStream input) throws IOException {
final List<String> lines = Lists.newArrayList();
readAllLines(input, new Callback<String>() {
@Override
public void handle(String line) {
lines.add(line);
}
});
return lines;
}
public static void readAllLines(InputStream input, Callback<String> callback) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, Charsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
callback.handle(line);
}
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.util;
/**
*
*/
public interface Callback<T> {
void handle(T t);
}

View File

@ -0,0 +1,92 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.cli;
import org.elasticsearch.common.Strings;
import org.elasticsearch.test.ElasticsearchTestCase;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
/**
*
*/
public class CliToolTestCase extends ElasticsearchTestCase {
protected static String[] args(String command) {
if (!Strings.hasLength(command)) {
return Strings.EMPTY_ARRAY;
}
return command.split("\\s+");
}
public static class TerminalMock extends Terminal {
private static final PrintWriter DEV_NULL = new PrintWriter(new DevNullWriter());
public TerminalMock() {
super(Verbosity.NORMAL);
}
public TerminalMock(Verbosity verbosity) {
super(verbosity);
}
@Override
protected void doPrint(String msg, Object... args) {
}
@Override
public String readText(String text, Object... args) {
return null;
}
@Override
public char[] readSecret(String text, Object... args) {
return new char[0];
}
@Override
public void print(String msg, Object... args) {
}
@Override
public PrintWriter writer() {
return DEV_NULL;
}
private static class DevNullWriter extends Writer {
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
}
@Override
public void flush() throws IOException {
}
@Override
public void close() throws IOException {
}
}
}
}

View File

@ -0,0 +1,343 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.cli;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.cli.CommandLine;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.junit.Test;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
/**
*
*/
public class CliToolTests extends CliToolTestCase {
@Test
public void testOK() throws Exception {
Terminal terminal = new TerminalMock();
final AtomicReference<Boolean> executed = new AtomicReference<>(false);
final NamedCommand cmd = new NamedCommand("cmd", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) {
executed.set(true);
return CliTool.ExitStatus.OK;
}
};
SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
int status = tool.execute();
assertExecuted(status, CliTool.ExitStatus.OK, executed, true);
}
@Test
public void testUsageError() throws Exception {
Terminal terminal = new TerminalMock();
final AtomicReference<Boolean> executed = new AtomicReference<>(false);
final NamedCommand cmd = new NamedCommand("cmd", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) {
executed.set(true);
return CliTool.ExitStatus.USAGE;
}
};
SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
int status = tool.execute();
assertExecuted(status, CliTool.ExitStatus.USAGE, executed, true);
}
@Test
public void testIOError() throws Exception {
Terminal terminal = new TerminalMock();
final AtomicReference<Boolean> executed = new AtomicReference<>(false);
final NamedCommand cmd = new NamedCommand("cmd", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
executed.set(true);
throw new IOException("io error");
}
};
SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
int status = tool.execute();
assertExecuted(status, CliTool.ExitStatus.IO_ERROR, executed, true);
}
@Test
public void testCodeError() throws Exception {
Terminal terminal = new TerminalMock();
final AtomicReference<Boolean> executed = new AtomicReference<>(false);
final NamedCommand cmd = new NamedCommand("cmd", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
executed.set(true);
throw new Exception("random error");
}
};
SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
int status = tool.execute();
assertExecuted(status, CliTool.ExitStatus.CODE_ERROR, executed, true);
}
@Test
public void testMultiCommand() {
Terminal terminal = new TerminalMock();
int count = randomIntBetween(2, 7);
final AtomicReference<Boolean>[] executed = new AtomicReference[count];
for (int i = 0; i < executed.length; i++) {
executed[i] = new AtomicReference<>(false);
}
NamedCommand[] cmds = new NamedCommand[count];
for (int i = 0; i < count; i++) {
final int index = i;
cmds[i] = new NamedCommand("cmd" + index, terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
executed[index].set(true);
return CliTool.ExitStatus.OK;
}
};
}
MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds);
int cmdIndex = randomIntBetween(0, count-1);
int status = tool.execute("cmd" + cmdIndex);
assertThat(status, is(CliTool.ExitStatus.OK.status()));
for (int i = 0; i < executed.length; i++) {
assertThat(executed[i].get(), is(i == cmdIndex));
}
}
@Test
public void testMultiCommand_UnknownCommand() {
Terminal terminal = new TerminalMock();
int count = randomIntBetween(2, 7);
final AtomicReference<Boolean>[] executed = new AtomicReference[count];
for (int i = 0; i < executed.length; i++) {
executed[i] = new AtomicReference<>(false);
}
NamedCommand[] cmds = new NamedCommand[count];
for (int i = 0; i < count; i++) {
final int index = i;
cmds[i] = new NamedCommand("cmd" + index, terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
executed[index].set(true);
return CliTool.ExitStatus.OK;
}
};
}
MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds);
int status = tool.execute("cmd" + count); // "cmd" + count doesn't exist
assertThat(status, is(CliTool.ExitStatus.USAGE.status()));
for (int i = 0; i < executed.length; i++) {
assertThat(executed[i].get(), is(false));
}
}
@Test
public void testSingleCommand_ToolHelp() throws Exception {
final AtomicInteger writeCounter = new AtomicInteger(0);
Terminal terminal = new TerminalMock() {
@Override
public void doPrint(String msg, Object... args) {
int count = writeCounter.incrementAndGet();
switch (count) {
case 1:
assertThat(msg, equalTo("\n"));
break;
case 2:
assertThat(msg, equalTo("cmd1 help\n"));
break;
case 3:
assertThat(msg, equalTo("\n"));
break;
default:
fail("written more than expected");
}
}
};
final AtomicReference<Boolean> executed = new AtomicReference<>(false);
final NamedCommand cmd = new NamedCommand("cmd1", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
executed.set(true);
throw new IOException("io error");
}
};
SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
int status = tool.execute(args("-h"));
assertExecuted(status, CliTool.ExitStatus.OK, writeCounter, 3);
}
@Test
public void testMultiCommand_ToolHelp() {
final AtomicInteger writeCounter = new AtomicInteger(0);
Terminal terminal = new TerminalMock() {
@Override
public void doPrint(String msg, Object... args) {
int count = writeCounter.incrementAndGet();
switch (count) {
case 1:
assertThat(msg, equalTo("\n"));
break;
case 2:
assertThat(msg, equalTo("tool help\n"));
break;
case 3:
assertThat(msg, equalTo("\n"));
break;
default:
fail("written more than expected");
}
}
};
NamedCommand[] cmds = new NamedCommand[2];
cmds[0] = new NamedCommand("cmd0", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
return CliTool.ExitStatus.OK;
}
};
cmds[1] = new NamedCommand("cmd1", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
return CliTool.ExitStatus.OK;
}
};
MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds);
int status = tool.execute(args("-h"));
assertExecuted(status, CliTool.ExitStatus.OK, writeCounter, 3);
}
@Test
public void testMultiCommand_CmdHelp() {
final AtomicInteger writeCounter = new AtomicInteger(0);
Terminal terminal = new TerminalMock() {
@Override
public void doPrint(String msg, Object... args) {
int count = writeCounter.incrementAndGet();
switch (count) {
case 1:
assertThat(msg, equalTo("\n"));
break;
case 2:
assertThat(msg, equalTo("cmd1 help\n"));
break;
case 3:
assertThat(msg, equalTo("\n"));
break;
default:
fail("written more than expected");
}
}
};
NamedCommand[] cmds = new NamedCommand[2];
cmds[0] = new NamedCommand("cmd0", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
return CliTool.ExitStatus.OK;
}
};
cmds[1] = new NamedCommand("cmd1", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
return CliTool.ExitStatus.OK;
}
};
MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds);
int status = tool.execute(args("cmd1 -h"));
assertExecuted(status, CliTool.ExitStatus.OK, writeCounter, 3);
}
private static void assertExecuted(int actualStatus, CliTool.ExitStatus expectedStatus, AtomicReference<Boolean> actualExecuted, boolean expectedExecuted) {
assertThat(actualExecuted.get(), is(expectedExecuted));
assertThat(actualStatus, is(expectedStatus.status()));
}
private static void assertExecuted(int actualStatus, CliTool.ExitStatus expectedStatus, AtomicInteger actualExecuted, int expectedExecuted) {
assertThat(actualExecuted.get(), is(expectedExecuted));
assertThat(actualStatus, is(expectedStatus.status()));
}
private static class SingleCmdTool extends CliTool {
private final Command command;
private SingleCmdTool(String name, Terminal terminal, NamedCommand command) {
super(CliToolConfig.config(name, SingleCmdTool.class)
.cmds(cmd(command.name, command.getClass()))
.build(), terminal);
this.command = command;
}
@Override
protected Command parse(String cmdName, CommandLine cli) throws Exception {
return command;
}
}
private static class MultiCmdTool extends CliTool {
private final Map<String, Command> commands;
private MultiCmdTool(String name, Terminal terminal, NamedCommand... commands) {
super(CliToolConfig.config(name, MultiCmdTool.class)
.cmds(cmds(commands))
.build(), terminal);
ImmutableMap.Builder<String, Command> commandByName = ImmutableMap.builder();
for (int i = 0; i < commands.length; i++) {
commandByName.put(commands[i].name, commands[i]);
}
this.commands = commandByName.build();
}
@Override
protected Command parse(String cmdName, CommandLine cli) throws Exception {
return commands.get(cmdName);
}
private static CliToolConfig.Cmd[] cmds(NamedCommand... commands) {
CliToolConfig.Cmd[] cmds = new CliToolConfig.Cmd[commands.length];
for (int i = 0; i < commands.length; i++) {
cmds[i] = cmd(commands[i].name, commands[i].getClass()).build();
}
return cmds;
}
}
private static abstract class NamedCommand extends CliTool.Command {
private final String name;
private NamedCommand(String name, Terminal terminal) {
super(terminal);
this.name = name;
}
}
}

View File

@ -0,0 +1,89 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.cli;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicReference;
import static org.hamcrest.Matchers.*;
/**
*
*/
public class TerminalTests extends CliToolTestCase {
@Test
public void testVerbosity() throws Exception {
final AtomicReference<Boolean> printed = new AtomicReference<>(false);
Terminal terminal = new TerminalMock(Terminal.Verbosity.SILENT) {
@Override
protected void doPrint(String msg, Object... args) {
printed.set(true);
assertThat(msg, equalTo("text"));
}
};
terminal.print(Terminal.Verbosity.SILENT, "text");
assertPrinted(printed);
terminal.print(Terminal.Verbosity.NORMAL, "text");
assertNotPrinted(printed);
terminal.print(Terminal.Verbosity.VERBOSE, "text");
assertNotPrinted(printed);
terminal = new TerminalMock(Terminal.Verbosity.NORMAL) {
@Override
protected void doPrint(String msg, Object... args) {
printed.set(true);
assertThat(msg, equalTo("text"));
}
};
terminal.print(Terminal.Verbosity.SILENT, "text");
assertPrinted(printed);
terminal.print(Terminal.Verbosity.NORMAL, "text");
assertPrinted(printed);
terminal.print(Terminal.Verbosity.VERBOSE, "text");
assertNotPrinted(printed);
terminal = new TerminalMock(Terminal.Verbosity.VERBOSE) {
@Override
protected void doPrint(String msg, Object... args) {
printed.set(true);
assertThat(msg, equalTo("text"));
}
};
terminal.print(Terminal.Verbosity.SILENT, "text");
assertPrinted(printed);
terminal.print(Terminal.Verbosity.NORMAL, "text");
assertPrinted(printed);
terminal.print(Terminal.Verbosity.VERBOSE, "text");
assertPrinted(printed);
}
private static void assertPrinted(AtomicReference<Boolean> actual) {
assertThat(actual.get(), is(true));
actual.set(false); // resetting
}
private static void assertNotPrinted(AtomicReference<Boolean> actual) {
assertThat(actual.get(), is(false));
}
}

View File

@ -0,0 +1 @@
cmd1 help

View File

@ -0,0 +1 @@
tool help