blob: 7133d9c3d4f97501e8871bbc9127ef5129ae3ff5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.apache.bookkeeper.tools.framework;
import com.beust.jcommander.JCommander;
import com.google.common.base.Strings;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.IntStream;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.bookkeeper.tools.framework.CliSpec.Builder;
/**
* Cli to execute {@link CliSpec}.
*/
@Slf4j
public class Cli<CliFlagsT extends CliFlags> {
private final CliSpec<CliFlagsT> spec;
private final JCommander commander;
@Getter(AccessLevel.PACKAGE)
private final Map<String, Command> commandMap;
private final Function<CliFlagsT, Boolean> runFunc;
private final String cmdPath;
private final PrintStream console;
public Cli(CliSpec<CliFlagsT> spec) {
this.spec = updateSpecWithDefaultValues(spec);
this.cmdPath = getCmdPath(spec);
this.commandMap = new TreeMap<>();
this.console = setupConsole(spec);
this.commander = setupCli(cmdPath, spec, commandMap);
if (null == spec.runFunc()) {
this.runFunc = (args) -> {
usage();
return false;
};
} else {
this.runFunc = spec.runFunc();
}
// inject default commands if needed
if (!spec.commands().isEmpty() && !hasCommand("help")) {
commandMap.put("help", new HelpCommand<>(this));
}
}
static <CliFlagsT extends CliFlags> String getCmdPath(CliSpec<CliFlagsT> spec) {
if (Strings.isNullOrEmpty(spec.parent())) {
return spec.name();
} else {
return spec.parent() + " " + spec.name();
}
}
static <CliFlagsT extends CliFlags> PrintStream setupConsole(CliSpec<CliFlagsT> spec) {
if (null == spec.console()) {
return System.out;
} else {
return spec.console();
}
}
CliSpec<CliFlagsT> updateSpecWithDefaultValues(CliSpec<CliFlagsT> spec) {
Builder<CliFlagsT> builder = CliSpec.newBuilder(spec);
if (Strings.isNullOrEmpty(spec.usage())) {
// simple command
if (spec.commands().isEmpty()) {
builder.withUsage(
String.format(
"%s [flags] " + spec.argumentsUsage(),
getCmdPath(spec)));
// command group
} else {
builder.withUsage(
String.format(
"%s [flags] [command] [command options]",
getCmdPath(spec)));
}
}
if (!spec.commands().isEmpty() && Strings.isNullOrEmpty(spec.tailer())) {
builder.withTailer(
String.format(
"Use \"%s [command] --help\" or \"%s help [command]\" for "
+ "more information about a command",
getCmdPath(spec), getCmdPath(spec)));
}
return builder.build();
}
String name() {
return spec.name();
}
String cmdPath() {
return cmdPath;
}
boolean hasCommand(String command) {
return commandMap.containsKey(command);
}
Command getCommand(String command) {
return commandMap.get(command);
}
static <CliFlagsT extends CliFlags> JCommander setupCli(String cmdPath,
CliSpec<CliFlagsT> spec,
Map<String, Command> commandMap) {
JCommander commander = new JCommander();
commander.setProgramName(cmdPath);
if (null != spec.flags()) {
commander.addObject(spec.flags());
}
for (Command cmd : spec.commands()) {
commandMap.put(cmd.name(), cmd);
}
// trigger command to generate help information
StringBuilder usageBuilder = new StringBuilder();
commander.usage(usageBuilder);
return commander;
}
protected void console(String msg) {
console().println(msg);
}
protected PrintStream console() {
return console;
}
void usage() {
usage("");
}
void usage(String errorMsg) {
if (Strings.isNullOrEmpty(errorMsg)) {
CommandUtils.printDescription(console(), 0, 0, spec.description());
console("");
} else {
CommandUtils.printDescription(console(), 0, 0, "Error : " + errorMsg);
console("");
}
// usage section
CommandUtils.printUsage(console(), spec.usage());
// command section
CommandUtils.printAvailableCommands(commandMap, console());
// flags section
if (!spec.isCommandGroup()) {
CommandUtils.printAvailableFlags(commander, console());
}
if (!spec.commands().isEmpty()) {
// tailer section
CommandUtils.printDescription(console(), 0, 0, spec.tailer());
}
}
@SuppressWarnings("unchecked")
boolean run(String[] args) {
// the cli has commands and args is empty
if (!spec.commands().isEmpty() && args.length == 0) {
usage();
return false;
}
int cmdPos = IntStream.range(0, args.length)
.filter(pos -> commandMap.containsKey(args[pos]))
.findFirst()
.orElse(args.length);
String[] flagArgs = Arrays.copyOfRange(args, 0, cmdPos);
// if this cli is a sub-command group, the flag args should be empty
if (spec.isCommandGroup() && flagArgs.length > 0) {
usage();
return false;
}
// parse flags
if (flagArgs.length != 0) {
// skip parsing flags
try {
commander.parse(flagArgs);
} catch (Exception e) {
usage(e.getMessage());
return false;
}
}
// print help messages
if (spec.flags().help) {
usage();
return false;
}
// found a sub command
if (cmdPos == args.length) {
return runFunc.apply(spec.flags());
} else {
String cmd = args[cmdPos];
Command command = commandMap.get(cmd);
String[] subCmdArgs = Arrays.copyOfRange(
args, cmdPos + 1, args.length);
try {
return command.apply(spec.flags(), subCmdArgs);
} catch (Exception e) {
e.printStackTrace(spec.console());
usage(e.getMessage());
return false;
}
}
}
public static <CliOptsT extends CliFlags> int runCli(
CliSpec<CliOptsT> spec, String[] args) {
Cli<CliOptsT> cli = new Cli<>(spec);
return cli.run(args) ? 0 : -1;
}
public static <CliOptsT extends CliFlags> void printUsage(CliSpec<CliOptsT> spec) {
Cli<CliOptsT> cli = new Cli<>(spec);
cli.usage();
}
}