1 /* 2 * Copyright 2016, Google LLC 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * Neither the name of Google LLC nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 package com.android.tools.smali.util.jcommander; 32 33 import com.beust.jcommander.JCommander; 34 import com.beust.jcommander.ParameterDescription; 35 import com.beust.jcommander.Parameters; 36 import com.google.common.base.Joiner; 37 import com.google.common.collect.Iterables; 38 import com.google.common.collect.Lists; 39 import com.android.tools.smali.util.WrappedIndentingWriter; 40 41 import javax.annotation.Nonnull; 42 import java.io.IOException; 43 import java.io.StringWriter; 44 import java.util.Arrays; 45 import java.util.Collections; 46 import java.util.Comparator; 47 import java.util.List; 48 import java.util.Map.Entry; 49 import java.util.regex.Matcher; 50 import java.util.regex.Pattern; 51 52 public class HelpFormatter { 53 54 private int width = 80; 55 56 @Nonnull width(int width)57 public HelpFormatter width(int width) { 58 this.width = width; 59 return this; 60 } 61 62 @Nonnull getExtendedParameters(JCommander jc)63 private static ExtendedParameters getExtendedParameters(JCommander jc) { 64 ExtendedParameters anno = jc.getObjects().get(0).getClass().getAnnotation(ExtendedParameters.class); 65 if (anno == null) { 66 throw new IllegalStateException("All commands should have an ExtendedParameters annotation"); 67 } 68 return anno; 69 } 70 71 @Nonnull getCommandAliases(JCommander jc)72 private static List<String> getCommandAliases(JCommander jc) { 73 return Lists.newArrayList(getExtendedParameters(jc).commandAliases()); 74 } 75 includeParametersInUsage(@onnull JCommander jc)76 private static boolean includeParametersInUsage(@Nonnull JCommander jc) { 77 return getExtendedParameters(jc).includeParametersInUsage(); 78 } 79 80 @Nonnull getPostfixDescription(@onnull JCommander jc)81 private static String getPostfixDescription(@Nonnull JCommander jc) { 82 return getExtendedParameters(jc).postfixDescription(); 83 } 84 getParameterArity(ParameterDescription param)85 private int getParameterArity(ParameterDescription param) { 86 if (param.getParameter().arity() > 0) { 87 return param.getParameter().arity(); 88 } 89 Class<?> type = param.getParameterized().getType(); 90 if ((type == boolean.class || type == Boolean.class)) { 91 return 0; 92 } 93 return 1; 94 } 95 getSortedParameters(JCommander jc)96 private List<ParameterDescription> getSortedParameters(JCommander jc) { 97 List<ParameterDescription> parameters = Lists.newArrayList(jc.getParameters()); 98 99 final Pattern pattern = Pattern.compile("^-*(.*)$"); 100 101 Collections.sort(parameters, new Comparator<ParameterDescription>() { 102 @Override public int compare(ParameterDescription o1, ParameterDescription o2) { 103 String s1; 104 Matcher matcher = pattern.matcher(o1.getParameter().names()[0]); 105 if (matcher.matches()) { 106 s1 = matcher.group(1); 107 } else { 108 throw new IllegalStateException(); 109 } 110 111 String s2; 112 matcher = pattern.matcher(o2.getParameter().names()[0]); 113 if (matcher.matches()) { 114 s2 = matcher.group(1); 115 } else { 116 throw new IllegalStateException(); 117 } 118 119 return s1.compareTo(s2); 120 } 121 }); 122 return parameters; 123 } 124 125 @Nonnull format(@onnull JCommander... jc)126 public String format(@Nonnull JCommander... jc) { 127 return format(Arrays.asList(jc)); 128 } 129 130 @Nonnull format(@onnull List<JCommander> commandHierarchy)131 public String format(@Nonnull List<JCommander> commandHierarchy) { 132 try { 133 StringWriter stringWriter = new StringWriter(); 134 WrappedIndentingWriter writer = new WrappedIndentingWriter(stringWriter, width - 5, width); 135 136 JCommander leafJc = Iterables.getLast(commandHierarchy); 137 138 writer.write("usage:"); 139 writer.indent(2); 140 141 for (JCommander jc: commandHierarchy) { 142 writer.write(" "); 143 writer.write(ExtendedCommands.commandName(jc)); 144 } 145 146 if (includeParametersInUsage(leafJc)) { 147 for (ParameterDescription param : leafJc.getParameters()) { 148 if (!param.getParameter().hidden()) { 149 writer.write(" ["); 150 writer.write(param.getParameter().getParameter().names()[0]); 151 writer.write("]"); 152 } 153 } 154 } else { 155 if (!leafJc.getParameters().isEmpty()) { 156 writer.write(" [<options>]"); 157 } 158 } 159 160 if (!leafJc.getCommands().isEmpty()) { 161 writer.write(" [<command [<args>]]"); 162 } 163 164 if (leafJc.getMainParameter() != null) { 165 String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter()); 166 if (argumentNames.length == 0) { 167 writer.write(" <args>"); 168 } else { 169 String argumentName = argumentNames[0]; 170 boolean writeAngleBrackets = !argumentName.startsWith("<") && !argumentName.startsWith("["); 171 writer.write(" "); 172 if (writeAngleBrackets) { 173 writer.write("<"); 174 } 175 writer.write(argumentNames[0]); 176 if (writeAngleBrackets) { 177 writer.write(">"); 178 } 179 } 180 } 181 182 writer.deindent(2); 183 184 String commandDescription = ExtendedCommands.getCommandDescription(leafJc); 185 if (commandDescription != null) { 186 writer.write("\n"); 187 writer.write(commandDescription); 188 } 189 190 if (!leafJc.getParameters().isEmpty() || leafJc.getMainParameter() != null) { 191 writer.write("\n\nOptions:"); 192 writer.indent(2); 193 for (ParameterDescription param : getSortedParameters(leafJc)) { 194 if (!param.getParameter().hidden()) { 195 writer.write("\n"); 196 writer.indent(4); 197 if (!param.getNames().isEmpty()) { 198 writer.write(Joiner.on(',').join(param.getParameter().names())); 199 } 200 if (getParameterArity(param) > 0) { 201 String[] argumentNames = ExtendedCommands.parameterArgumentNames(param); 202 for (int i = 0; i < getParameterArity(param); i++) { 203 writer.write(" "); 204 if (i < argumentNames.length) { 205 writer.write("<"); 206 writer.write(argumentNames[i]); 207 writer.write(">"); 208 } else { 209 writer.write("<arg>"); 210 } 211 } 212 } 213 if (param.getDescription() != null && !param.getDescription().isEmpty()) { 214 writer.write(" - "); 215 writer.write(param.getDescription()); 216 } 217 if (param.getDefault() != null) { 218 String defaultValue = null; 219 if (param.getParameterized().getType() == Boolean.class || 220 param.getParameterized().getType() == Boolean.TYPE) { 221 if ((Boolean)param.getDefault()) { 222 defaultValue = "True"; 223 } 224 } else if (List.class.isAssignableFrom(param.getParameterized().getType())) { 225 if (!((List)param.getDefault()).isEmpty()) { 226 defaultValue = param.getDefault().toString(); 227 } 228 } else { 229 defaultValue = param.getDefault().toString(); 230 } 231 if (defaultValue != null) { 232 writer.write(" (default: "); 233 writer.write(defaultValue); 234 writer.write(")"); 235 } 236 } 237 writer.deindent(4); 238 } 239 } 240 241 if (leafJc.getMainParameter() != null) { 242 String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter()); 243 writer.write("\n"); 244 writer.indent(4); 245 if (argumentNames.length > 0) { 246 writer.write("<"); 247 writer.write(argumentNames[0]); 248 writer.write(">"); 249 } else { 250 writer.write("<args>"); 251 } 252 253 if (leafJc.getMainParameterDescription() != null) { 254 writer.write(" - "); 255 writer.write(leafJc.getMainParameterDescription()); 256 } 257 writer.deindent(4); 258 } 259 writer.deindent(2); 260 } 261 262 if (!leafJc.getCommands().isEmpty()) { 263 writer.write("\n\nCommands:"); 264 writer.indent(2); 265 266 267 List<Entry<String, JCommander>> entryList = Lists.newArrayList(leafJc.getCommands().entrySet()); 268 Collections.sort(entryList, new Comparator<Entry<String, JCommander>>() { 269 @Override public int compare(Entry<String, JCommander> o1, Entry<String, JCommander> o2) { 270 return o1.getKey().compareTo(o2.getKey()); 271 } 272 }); 273 274 for (Entry<String, JCommander> entry : entryList) { 275 String commandName = entry.getKey(); 276 JCommander command = entry.getValue(); 277 278 Object arg = command.getObjects().get(0); 279 Parameters parametersAnno = arg.getClass().getAnnotation(Parameters.class); 280 if (!parametersAnno.hidden()) { 281 writer.write("\n"); 282 writer.indent(4); 283 writer.write(commandName); 284 List<String> aliases = getCommandAliases(command); 285 if (!aliases.isEmpty()) { 286 writer.write("("); 287 writer.write(Joiner.on(',').join(aliases)); 288 writer.write(")"); 289 } 290 291 String commandDesc = leafJc.getCommandDescription(commandName); 292 if (commandDesc != null) { 293 writer.write(" - "); 294 writer.write(commandDesc); 295 } 296 writer.deindent(4); 297 } 298 } 299 writer.deindent(2); 300 } 301 302 String postfixDescription = getPostfixDescription(leafJc); 303 if (!postfixDescription.isEmpty()) { 304 writer.write("\n\n"); 305 writer.write(postfixDescription); 306 } 307 308 writer.flush(); 309 310 return stringWriter.getBuffer().toString(); 311 } catch (IOException ex) { 312 throw new RuntimeException(ex); 313 } 314 } 315 } 316