/* * Copyright 2012-2017 the original author or authors. * * 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 org.springframework.boot.cli.command; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.springframework.boot.cli.command.status.ExitStatus; import org.springframework.boot.cli.util.Log; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Main class used to run {@link Command}s. * * @author Phillip Webb * @see #addCommand(Command) * @see CommandRunner#runAndHandleErrors(String[]) */ public class CommandRunner implements Iterable<Command> { private static final Set<CommandException.Option> NO_EXCEPTION_OPTIONS = EnumSet .noneOf(CommandException.Option.class); private final String name; private final List<Command> commands = new ArrayList<>(); private Class<?>[] optionCommandClasses = {}; private Class<?>[] hiddenCommandClasses = {}; /** * Create a new {@link CommandRunner} instance. * @param name the name of the runner or {@code null} */ public CommandRunner(String name) { this.name = (StringUtils.hasLength(name) ? name + " " : ""); } /** * Return the name of the runner or an empty string. Non-empty names will include a * trailing space character so that they can be used as a prefix. * @return the name of the runner */ public String getName() { return this.name; } /** * Add the specified commands. * @param commands the commands to add */ public void addCommands(Iterable<Command> commands) { Assert.notNull(commands, "Commands must not be null"); for (Command command : commands) { addCommand(command); } } /** * Add the specified command. * @param command the command to add. */ public void addCommand(Command command) { Assert.notNull(command, "Command must not be null"); this.commands.add(command); } /** * Set the command classes which should be considered option commands. An option * command is a special type of command that usually makes more sense to present as if * it is an option. For example '--version'. * @param commandClasses the classes of option commands. * @see #isOptionCommand(Command) */ public void setOptionCommands(Class<?>... commandClasses) { Assert.notNull(commandClasses, "CommandClasses must not be null"); this.optionCommandClasses = commandClasses; } /** * Set the command classes which should be hidden (i.e. executed but not displayed in * the available commands list). * @param commandClasses the classes of hidden commands */ public void setHiddenCommands(Class<?>... commandClasses) { Assert.notNull(commandClasses, "CommandClasses must not be null"); this.hiddenCommandClasses = commandClasses; } /** * Returns if the specified command is an option command. * @param command the command to test * @return {@code true} if the command is an option command * @see #setOptionCommands(Class...) */ public boolean isOptionCommand(Command command) { return isCommandInstanceOf(command, this.optionCommandClasses); } private boolean isHiddenCommand(Command command) { return isCommandInstanceOf(command, this.hiddenCommandClasses); } private boolean isCommandInstanceOf(Command command, Class<?>[] commandClasses) { for (Class<?> commandClass : commandClasses) { if (commandClass.isInstance(command)) { return true; } } return false; } @Override public Iterator<Command> iterator() { return getCommands().iterator(); } protected final List<Command> getCommands() { return Collections.unmodifiableList(this.commands); } /** * Find a command by name. * @param name the name of the command * @return the command or {@code null} if not found */ public Command findCommand(String name) { for (Command candidate : this.commands) { String candidateName = candidate.getName(); if (candidateName.equals(name) || (isOptionCommand(candidate) && ("--" + candidateName).equals(name))) { return candidate; } } return null; } /** * Run the appropriate and handle and errors. * @param args the input arguments * @return a return status code (non boot is used to indicate an error) */ public int runAndHandleErrors(String... args) { String[] argsWithoutDebugFlags = removeDebugFlags(args); boolean debug = argsWithoutDebugFlags.length != args.length; if (debug) { System.setProperty("debug", "true"); } try { ExitStatus result = run(argsWithoutDebugFlags); // The caller will hang up if it gets a non-zero status if (result != null && result.isHangup()) { return (result.getCode() > 0 ? result.getCode() : 0); } return 0; } catch (NoArgumentsException ex) { showUsage(); return 1; } catch (Exception ex) { return handleError(debug, ex); } } private String[] removeDebugFlags(String[] args) { List<String> rtn = new ArrayList<>(args.length); boolean appArgsDetected = false; for (String arg : args) { // Allow apps to have a -d argument appArgsDetected |= "--".equals(arg); if (("-d".equals(arg) || "--debug".equals(arg)) && !appArgsDetected) { continue; } rtn.add(arg); } return rtn.toArray(new String[rtn.size()]); } /** * Parse the arguments and run a suitable command. * @param args the arguments * @return the outcome of the command * @throws Exception if the command fails */ protected ExitStatus run(String... args) throws Exception { if (args.length == 0) { throw new NoArgumentsException(); } String commandName = args[0]; String[] commandArguments = Arrays.copyOfRange(args, 1, args.length); Command command = findCommand(commandName); if (command == null) { throw new NoSuchCommandException(commandName); } beforeRun(command); try { return command.run(commandArguments); } finally { afterRun(command); } } /** * Subclass hook called before a command is run. * @param command the command about to run */ protected void beforeRun(Command command) { } /** * Subclass hook called after a command has run. * @param command the command that has run */ protected void afterRun(Command command) { } private int handleError(boolean debug, Exception ex) { Set<CommandException.Option> options = NO_EXCEPTION_OPTIONS; if (ex instanceof CommandException) { options = ((CommandException) ex).getOptions(); if (options.contains(CommandException.Option.RETHROW)) { throw (CommandException) ex; } } boolean couldNotShowMessage = false; if (!options.contains(CommandException.Option.HIDE_MESSAGE)) { couldNotShowMessage = !errorMessage(ex.getMessage()); } if (options.contains(CommandException.Option.SHOW_USAGE)) { showUsage(); } if (debug || couldNotShowMessage || options.contains(CommandException.Option.STACK_TRACE)) { printStackTrace(ex); } return 1; } protected boolean errorMessage(String message) { Log.error(message == null ? "Unexpected error" : message); return message != null; } protected void showUsage() { Log.infoPrint("usage: " + this.name); for (Command command : this.commands) { if (isOptionCommand(command)) { Log.infoPrint("[--" + command.getName() + "] "); } } Log.info(""); Log.info(" <command> [<args>]"); Log.info(""); Log.info("Available commands are:"); for (Command command : this.commands) { if (!isOptionCommand(command) && !isHiddenCommand(command)) { String usageHelp = command.getUsageHelp(); String description = command.getDescription(); Log.info(String.format("%n %1$s %2$-15s%n %3$s", command.getName(), (usageHelp == null ? "" : usageHelp), (description == null ? "" : description))); } } Log.info(""); Log.info("Common options:"); Log.info(String.format("%n %1$s %2$-15s%n %3$s", "-d, --debug", "Verbose mode", "Print additional status information for the command you are running")); Log.info(""); Log.info(""); Log.info("See '" + this.name + "help <command>' for more information on a specific command."); } protected void printStackTrace(Exception ex) { Log.error(""); Log.error(ex); Log.error(""); } }