1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.loganalysis.util.config; 17 18 import com.android.loganalysis.util.ArrayUtil; 19 20 import java.lang.reflect.Field; 21 import java.util.ArrayList; 22 import java.util.Arrays; 23 import java.util.Collection; 24 import java.util.List; 25 import java.util.ListIterator; 26 27 /** 28 * Populates {@link Option} fields from parsed command line arguments. 29 * <p/> 30 * Strings in the passed-in String[] are parsed left-to-right. Each String is classified as a short 31 * option (such as "-v"), a long option (such as "--verbose"), an argument to an option (such as 32 * "out.txt" in "-f out.txt"), or a non-option positional argument. 33 * <p/> 34 * Each option argument must map to one or more {@link Option} fields. A long option maps to the 35 * {@link Option#name}, and a short option maps to {@link Option#shortName}. Each 36 * {@link Option#name()} and {@link Option#shortName()} must be unique with respect to all other 37 * {@link Option} fields within the same object. 38 * <p/> 39 * A single option argument can get mapped to multiple {@link Option} fields with the same name 40 * across multiple objects. {@link Option} arguments can be namespaced to uniquely refer to an 41 * {@link Option} field within a single object using that object's full class name or its 42 * {@link OptionClass#alias()} value separated by ':'. ie 43 * 44 * <pre> 45 * --classname:optionname optionvalue or 46 * --optionclassalias:optionname optionvalue. 47 * </pre> 48 * <p/> 49 * A simple short option is a "-" followed by a short option character. If the option requires an 50 * argument (which is true of any non-boolean option), it may be written as a separate parameter, 51 * but need not be. That is, "-f out.txt" and "-fout.txt" are both acceptable. 52 * <p/> 53 * It is possible to specify multiple short options after a single "-" as long as all (except 54 * possibly the last) do not require arguments. 55 * <p/> 56 * A long option begins with "--" followed by several characters. If the option requires an 57 * argument, it may be written directly after the option name, separated by "=", or as the next 58 * argument. (That is, "--file=out.txt" or "--file out.txt".) 59 * <p/> 60 * A boolean long option '--name' automatically gets a '--no-name' companion. Given an option 61 * "--flag", then, "--flag", "--no-flag", "--flag=true" and "--flag=false" are all valid, though 62 * neither "--flag true" nor "--flag false" are allowed (since "--flag" by itself is sufficient, the 63 * following "true" or "false" is interpreted separately). You can use "yes" and "no" as synonyms 64 * for "true" and "false". 65 * <p/> 66 * Each String not starting with a "-" and not a required argument of a previous option is a 67 * non-option positional argument, as are all successive Strings. Each String after a "--" is a 68 * non-option positional argument. 69 * <p/> 70 * The fields corresponding to options are updated as their options are processed. Any remaining 71 * positional arguments are returned as a List<String>. 72 * <p/> 73 * Here's a simple example: 74 * <p/> 75 * 76 * <pre> 77 * // Non-@Option fields will be ignored. 78 * class Options { 79 * @Option(name = "quiet", shortName = 'q') 80 * boolean quiet = false; 81 * 82 * // Here the user can use --no-color. 83 * @Option(name = "color") 84 * boolean color = true; 85 * 86 * @Option(name = "mode", shortName = 'm') 87 * String mode = "standard; // Supply a default just by setting the field. 88 * 89 * @Option(name = "port", shortName = 'p') 90 * int portNumber = 8888; 91 * 92 * // There's no need to offer a short name for rarely-used options. 93 * @Option(name = "timeout" ) 94 * double timeout = 1.0; 95 * 96 * @Option(name = "output-file", shortName = 'o' }) 97 * File output; 98 * 99 * // Multiple options are added to the collection. 100 * // The collection field itself must be non-null. 101 * @Option(name = "input-file", shortName = 'i') 102 * List<File> inputs = new ArrayList<File>(); 103 * 104 * } 105 * 106 * Options options = new Options(); 107 * List<String> posArgs = new OptionParser(options).parse("--input-file", "/tmp/file1.txt"); 108 * for (File inputFile : options.inputs) { 109 * if (!options.quiet) { 110 * ... 111 * } 112 * ... 113 * 114 * } 115 * 116 * </pre> 117 * 118 * See also: 119 * <ul> 120 * <li>the getopt(1) man page 121 * <li>Python's "optparse" module (http://docs.python.org/library/optparse.html) 122 * <li>the POSIX "Utility Syntax Guidelines" 123 * (http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap12.html#tag_12_02) 124 * <li>the GNU "Standards for Command Line Interfaces" 125 * (http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Interfaces) 126 * </ul> 127 * 128 * @see {@link OptionSetter} 129 */ 130 //TODO: Use libTF once this is copied over. 131 public class ArgsOptionParser extends OptionSetter { 132 133 static final String SHORT_NAME_PREFIX = "-"; 134 static final String OPTION_NAME_PREFIX = "--"; 135 136 /** the amount to indent an option field's description when displaying help */ 137 private static final int OPTION_DESCRIPTION_INDENT = 25; 138 139 /** 140 * Creates a {@link ArgsOptionParser} for a collection of objects. 141 * 142 * @param optionSources the config objects. 143 * @throws ConfigurationException if config objects is improperly configured. 144 */ 145 public ArgsOptionParser(Collection<Object> optionSources) throws ConfigurationException { 146 super(optionSources); 147 } 148 149 /** 150 * Creates a {@link ArgsOptionParser} for one or more objects. 151 * 152 * @param optionSources the config objects. 153 * @throws ConfigurationException if config objects is improperly configured. 154 */ 155 public ArgsOptionParser(Object... optionSources) throws ConfigurationException { 156 super(optionSources); 157 } 158 159 /** 160 * Parses the command-line arguments 'args', setting the @Option fields of the 'optionSource' 161 * provided to the constructor. 162 * 163 * @return a {@link List} of the positional arguments left over after processing all options. 164 * @throws ConfigurationException if error occurred parsing the arguments. 165 */ 166 public List<String> parse(String... args) throws ConfigurationException { 167 return parse(Arrays.asList(args)); 168 } 169 170 /** 171 * Alternate {@link #parse(String... args)} method that takes a {@link List} of arguments 172 * 173 * @return a {@link List} of the positional arguments left over after processing all options. 174 * @throws ConfigurationException if error occurred parsing the arguments. 175 */ 176 public List<String> parse(List<String> args) throws ConfigurationException { 177 return parseOptions(args.listIterator()); 178 } 179 180 /** 181 * Validates that all fields marked as {@link Option#mandatory()} have been set. 182 * @throws ConfigurationException 183 */ 184 public void validateMandatoryOptions() throws ConfigurationException { 185 // Make sure that all mandatory options have been specified 186 List<String> missingOptions = new ArrayList<String>(getUnsetMandatoryOptions()); 187 if (!missingOptions.isEmpty()) { 188 throw new ConfigurationException(String.format("Found missing mandatory options: %s", 189 ArrayUtil.join(", ", missingOptions))); 190 } 191 } 192 193 private List<String> parseOptions(ListIterator<String> args) throws ConfigurationException { 194 final List<String> leftovers = new ArrayList<String>(); 195 196 // Scan 'args'. 197 while (args.hasNext()) { 198 final String arg = args.next(); 199 if (arg.equals(OPTION_NAME_PREFIX)) { 200 // "--" marks the end of options and the beginning of positional arguments. 201 break; 202 } else if (arg.startsWith(OPTION_NAME_PREFIX)) { 203 // A long option. 204 parseLongOption(arg, args); 205 } else if (arg.startsWith(SHORT_NAME_PREFIX)) { 206 // A short option. 207 parseGroupedShortOptions(arg, args); 208 } else { 209 // The first non-option marks the end of options. 210 leftovers.add(arg); 211 break; 212 } 213 } 214 215 // Package up the leftovers. 216 while (args.hasNext()) { 217 leftovers.add(args.next()); 218 } 219 return leftovers; 220 } 221 222 private void parseLongOption(String arg, ListIterator<String> args) 223 throws ConfigurationException { 224 // remove prefix to just get name 225 String name = arg.replaceFirst("^" + OPTION_NAME_PREFIX, ""); 226 String key = null; 227 String value = null; 228 229 // Support "--name=value" as well as "--name value". 230 final int equalsIndex = name.indexOf('='); 231 if (equalsIndex != -1) { 232 value = name.substring(equalsIndex + 1); 233 name = name.substring(0, equalsIndex); 234 } 235 236 if (value == null) { 237 if (isBooleanOption(name)) { 238 int idx = name.indexOf(NAMESPACE_SEPARATOR); 239 value = name.startsWith(BOOL_FALSE_PREFIX, idx + 1) ? "false" : "true"; 240 } else if (isMapOption(name)) { 241 key = grabNextValue(args, name, "for its key"); 242 value = grabNextValue(args, name, "for its value"); 243 } else { 244 value = grabNextValue(args, name); 245 } 246 } 247 if (isMapOption(name)) { 248 setOptionMapValue(name, key, value); 249 } else { 250 setOptionValue(name, value); 251 } 252 } 253 254 // Given boolean options a and b, and non-boolean option f, we want to allow: 255 // -ab 256 // -abf out.txt 257 // -abfout.txt 258 // (But not -abf=out.txt --- POSIX doesn't mention that either way, but GNU expressly forbids 259 // it.) 260 private void parseGroupedShortOptions(String arg, ListIterator<String> args) 261 throws ConfigurationException { 262 for (int i = 1; i < arg.length(); ++i) { 263 final String name = String.valueOf(arg.charAt(i)); 264 String value; 265 if (isBooleanOption(name)) { 266 value = "true"; 267 } else { 268 // We need a value. If there's anything left, we take the rest of this 269 // "short option". 270 if (i + 1 < arg.length()) { 271 value = arg.substring(i + 1); 272 i = arg.length() - 1; 273 } else { 274 value = grabNextValue(args, name); 275 } 276 } 277 setOptionValue(name, value); 278 } 279 } 280 281 /** 282 * Returns the next element of 'args' if there is one. Uses 'name' to construct a helpful error 283 * message. 284 * 285 * @param args the arg iterator 286 * @param name the name of current argument 287 * @throws ConfigurationException if no argument is present 288 * 289 * @returns the next element 290 */ 291 private String grabNextValue(ListIterator<String> args, String name) 292 throws ConfigurationException { 293 return grabNextValue(args, name, ""); 294 } 295 296 /** 297 * Returns the next element of 'args' if there is one. Uses 'name' to construct a helpful error 298 * message. 299 * 300 * @param args the arg iterator 301 * @param name the name of current argument 302 * @param detail a string to append to the ConfigurationException message, if one is thrown 303 * @throws ConfigurationException if no argument is present 304 * 305 * @returns the next element 306 */ 307 private String grabNextValue(ListIterator<String> args, String name, String detail) 308 throws ConfigurationException { 309 if (!args.hasNext()) { 310 String type = getTypeForOption(name); 311 throw new ConfigurationException(String.format("option '%s' requires a '%s' argument%s", 312 name, type, detail)); 313 } 314 return args.next(); 315 } 316 317 /** 318 * Output help text for all {@link Option} fields in <param>optionObject</param>. 319 * <p/> 320 * The help text for each option will be in the following format 321 * <pre> 322 * [-option_shortname, --option_name] [option_description] Default: 323 * [current option field's value in optionObject] 324 * </pre> 325 * The 'Default..." text will be omitted if the option field is null or empty. 326 * 327 * @param importantOnly if <code>true</code> only print help for the important options 328 * @param optionObject the object to print help text for 329 * @return a String containing user-friendly help text for all Option fields 330 */ 331 public static String getOptionHelp(boolean importantOnly, Object optionObject) { 332 StringBuilder out = new StringBuilder(); 333 Collection<Field> optionFields = OptionSetter.getOptionFieldsForClass( 334 optionObject.getClass()); 335 String eol = System.getProperty("line.separator"); 336 for (Field field : optionFields) { 337 final Option option = field.getAnnotation(Option.class); 338 String defaultValue = OptionSetter.getFieldValueAsString(field, optionObject); 339 String optionNameHelp = buildOptionNameHelp(field, option); 340 if (shouldOutputHelpForOption(importantOnly, option, defaultValue)) { 341 out.append(optionNameHelp); 342 // insert appropriate whitespace between the name help and the description, to 343 // ensure consistent alignment 344 int wsChars = 0; 345 if (optionNameHelp.length() >= OPTION_DESCRIPTION_INDENT) { 346 // name help is too long, break description onto next line 347 out.append(eol); 348 wsChars = OPTION_DESCRIPTION_INDENT; 349 } else { 350 // insert enough whitespace so option.description starts at 351 // OPTION_DESCRIPTION_INDENT 352 wsChars = OPTION_DESCRIPTION_INDENT - optionNameHelp.length(); 353 } 354 for (int i = 0; i < wsChars; ++i) { 355 out.append(' '); 356 } 357 out.append(option.description()); 358 out.append(getDefaultValueHelp(defaultValue)); 359 out.append(OptionSetter.getEnumFieldValuesAsString(field)); 360 out.append(eol); 361 } 362 } 363 return out.toString(); 364 } 365 366 /** 367 * Determine if help for given option should be displayed. 368 * 369 * @param importantOnly 370 * @param option 371 * @param defaultValue 372 * @return <code>true</code> if help for option should be displayed 373 */ 374 private static boolean shouldOutputHelpForOption(boolean importantOnly, Option option, 375 String defaultValue) { 376 if (!importantOnly) { 377 return true; 378 } 379 switch (option.importance()) { 380 case NEVER: 381 return false; 382 case IF_UNSET: 383 return defaultValue == null; 384 case ALWAYS: 385 return true; 386 } 387 return false; 388 } 389 390 /** 391 * Builds the 'name' portion of the help text for the given option field 392 * 393 * @param field 394 * @param option 395 * @return the help text that describes the option flags 396 */ 397 private static String buildOptionNameHelp(Field field, final Option option) { 398 StringBuilder optionNameBuilder = new StringBuilder(); 399 optionNameBuilder.append(" "); 400 if (option.shortName() != Option.NO_SHORT_NAME) { 401 optionNameBuilder.append(SHORT_NAME_PREFIX); 402 optionNameBuilder.append(option.shortName()); 403 optionNameBuilder.append(", "); 404 } 405 optionNameBuilder.append(OPTION_NAME_PREFIX); 406 try { 407 if (OptionSetter.isBooleanField(field)) { 408 optionNameBuilder.append("[no-]"); 409 } 410 } catch (ConfigurationException e) { 411 // ignore 412 } 413 optionNameBuilder.append(option.name()); 414 return optionNameBuilder.toString(); 415 } 416 417 /** 418 * Returns the help text describing the given default value 419 * 420 * @param defaultValue the default value 421 * @return the help text, or an empty {@link String} if <param>field</param> has no value 422 */ 423 private static String getDefaultValueHelp(String defaultValue) { 424 if (defaultValue == null) { 425 return ""; 426 } else { 427 return String.format(" Default: %s.", defaultValue); 428 } 429 } 430 } 431