/* * Copyright © 2012-2014 Cask Data, 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 co.cask.cdap.cli.command.system; import co.cask.cdap.cli.ArgumentName; import co.cask.cdap.cli.Categorized; import co.cask.cdap.cli.CommandCategory; import co.cask.cdap.cli.util.StringStyler; import co.cask.cdap.cli.util.table.TableRendererConfig; import co.cask.common.cli.Arguments; import co.cask.common.cli.Command; import co.cask.common.cli.CommandSet; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.HashMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import java.io.PrintStream; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * Prints helper text for all commands. */ public class HelpCommand implements Command { protected final Supplier<Iterable<CommandSet<Command>>> commands; private final TableRendererConfig tableRendererConfig; public HelpCommand(Supplier<Iterable<CommandSet<Command>>> commands, TableRendererConfig tableRendererConfig) { this.commands = commands; this.tableRendererConfig = tableRendererConfig; } @Override public void execute(Arguments arguments, PrintStream output) throws Exception { output.println(); output.println(); if (arguments.hasArgument(ArgumentName.COMMAND_CATEGORY.getName())) { // help with one category Multimap<String, Command> categorizedCommands = categorizeCommands(commands.get(), CommandCategory.GENERAL, Predicates.<Command>alwaysTrue()); String commandCategoryInput = arguments.get(ArgumentName.COMMAND_CATEGORY.getName()); CommandCategory category = CommandCategory.valueOfNameIgnoreCase(commandCategoryInput); List<Command> commandList = Lists.newArrayList(categorizedCommands.get(category.getName())); if (commandList.isEmpty()) { output.printf("No commands found in the %s category", category.getName()); output.println(); return; } printCommands(output, category.getName(), commandList); } else { // normal help Multimap<String, Command> categorizedCommands = categorizeCommands(commands.get(), CommandCategory.GENERAL, Predicates.<Command>alwaysTrue()); for (CommandCategory category : CommandCategory.values()) { List<Command> commandList = Lists.newArrayList(categorizedCommands.get(category.getName())); if (commandList.isEmpty()) { continue; } printCommands(output, category.getName(), commandList); } } } protected void printCommands(PrintStream output, String category, List<Command> commandList) { Collections.sort(commandList, new Comparator<Command>() { @Override public int compare(Command command, Command command2) { return command.getPattern().compareTo(command2.getPattern()); } }); output.println(StringStyler.bold(category)); output.println(); for (Command command : commandList) { printPattern(command.getPattern(), output, tableRendererConfig.getLineWidth(), 2); wrappedPrint(command.getDescription(), output, tableRendererConfig.getLineWidth(), 4); output.println(); } } protected Multimap<String, Command> categorizeCommands(Iterable<CommandSet<Command>> commandSets, CommandCategory defaultCategory, Predicate<Command> filter) { Multimap<String, Command> result = HashMultimap.create(); for (CommandSet<Command> commandSet : commandSets) { populate(result, commandSet, getCategory(commandSet), defaultCategory, filter); } return result; } /** * Recursive helper for {@link #categorizeCommands(Iterable, CommandCategory, Predicate)}. */ private void populate(Multimap<String, Command> result, CommandSet<Command> commandSet, Optional<String> parentCategory, CommandCategory defaultCategory, Predicate<Command> filter) { for (Command childCommand : Iterables.filter(commandSet.getCommands(), filter)) { Optional<String> commandCategory = getCategory(childCommand).or(parentCategory); result.put(commandCategory.or(defaultCategory.getName()), childCommand); } for (CommandSet<Command> childCommandSet : commandSet.getCommandSets()) { Optional<String> commandCategory = getCategory(childCommandSet).or(parentCategory); populate(result, childCommandSet, commandCategory, defaultCategory, filter); } } private Optional<String> getCategory(Object object) { if (object instanceof Categorized) { return Optional.of(((Categorized) object).getCategory()); } return Optional.absent(); } @Override public String getPattern() { return String.format("help [<%s>]", ArgumentName.COMMAND_CATEGORY); } @Override public String getDescription() { return String.format("Prints this helper text. Optionally, provide <%s> for help for a specific category.", ArgumentName.COMMAND_CATEGORY); } /** * Prints the given command pattern with text wrapping at the given column width. It prints multiple lines as: * * <pre>{@code * command sub-command <arg1> <arg2> * <arg3> [<optional-long-arg4>] * } * </pre> * * @param pattern the command pattern to print * @param output the {@link PrintStream} to write to * @param colWidth width of the column * @param prefixSpaces number of spaces as the prefix for each line printed */ private void printPattern(String pattern, PrintStream output, int colWidth, int prefixSpaces) { String prefix = Strings.repeat(" ", prefixSpaces); colWidth -= prefixSpaces; if (pattern.length() <= colWidth) { output.printf("%s%s", prefix, pattern); output.println(); return; } // Find the first <argument>, it is used for alignment in second line and onward. int startIdx = pattern.indexOf('<'); // If no '<', it shouldn't reach here (it should be a short command). If it does, just print it. if (startIdx < 0) { output.printf("%s%s", prefix, pattern); output.println(); return; } // First line should at least include the first <argument> int idx = pattern.lastIndexOf('>', colWidth) + 1; if (idx <= 0) { idx = pattern.indexOf('>', startIdx) + 1; if (idx <= 0) { idx = pattern.length(); } } // Make sure we include the closing ] of an optional argument if (idx < pattern.length() && pattern.charAt(idx) == ']') { idx++; } output.printf("%s%s", prefix, pattern.substring(0, idx)); output.println(); if ((idx + 1) < pattern.length()) { // For rest of the line, align them to the startIdx wrappedPrint(pattern.substring(idx + 1), output, colWidth, startIdx + prefixSpaces); } } /** * Prints the given string with text wrapping at the given column width. * * @param str the string to print * @param output the {@link PrintStream} to write to * @param colWidth width of the column * @param prefixSpaces number of spaces as the prefix for each line printed */ private void wrappedPrint(String str, PrintStream output, int colWidth, int prefixSpaces) { String prefix = Strings.repeat(" ", prefixSpaces); colWidth -= prefixSpaces; if (str.length() <= colWidth) { output.printf("%s%s", prefix, str); output.println(); return; } int beginIdx = 0; while (beginIdx < str.length()) { int idx; if (beginIdx + colWidth >= str.length()) { idx = str.length(); } else { idx = str.lastIndexOf(' ', beginIdx + colWidth); } // Cannot break line if no space found between beginIdx and (beginIdx + colWidth) // The best we can do is to look forward. // The line will be longer than colWidth though. if (idx < 0 || idx < beginIdx) { idx = str.indexOf(' ', beginIdx + colWidth); if (idx < 0) { idx = str.length(); } } output.printf("%s%s", prefix, str.substring(beginIdx, idx)); beginIdx = idx + 1; output.println(); } } }