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.tradefed.command; 17 18 import com.android.tradefed.config.ConfigurationException; 19 import com.android.tradefed.log.LogUtil.CLog; 20 import com.android.tradefed.util.QuotationAwareTokenizer; 21 22 import java.io.BufferedReader; 23 import java.io.File; 24 import java.io.FileReader; 25 import java.io.IOException; 26 import java.util.Arrays; 27 import java.util.Collection; 28 import java.util.HashMap; 29 import java.util.HashSet; 30 import java.util.LinkedList; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.Objects; 34 import java.util.regex.Matcher; 35 import java.util.regex.Pattern; 36 37 /** 38 * Parser for file that contains set of command lines. 39 * <p/> 40 * The syntax of the given file should be series of lines. Each line is a command; that is, a 41 * configuration plus its options: 42 * <pre> 43 * [options] config-name 44 * [options] config-name2 45 * ... 46 * </pre> 47 */ 48 public class CommandFileParser { 49 50 /** 51 * A pattern that matches valid macro usages and captures the name of the macro. 52 * Macro names must start with an alpha character, and may contain alphanumerics, underscores, 53 * or hyphens. 54 */ 55 private static final Pattern MACRO_PATTERN = Pattern.compile("([a-z][a-z0-9_-]*)\\(\\)", 56 Pattern.CASE_INSENSITIVE); 57 58 private Map<String, CommandLine> mMacros = new HashMap<String, CommandLine>(); 59 private Map<String, List<CommandLine>> mLongMacros = new HashMap<String, List<CommandLine>>(); 60 private List<CommandLine> mLines = new LinkedList<CommandLine>(); 61 62 private Collection<String> mIncludedFiles = new HashSet<String>(); 63 64 @SuppressWarnings("serial") 65 public static class CommandLine extends LinkedList<String> { 66 private final File mFile; 67 private final int mLineNumber; 68 CommandLine(File file, int lineNumber)69 CommandLine(File file, int lineNumber) { 70 super(); 71 mFile = file; 72 mLineNumber = lineNumber; 73 } 74 CommandLine(Collection<? extends String> c, File file, int lineNumber)75 CommandLine(Collection<? extends String> c, File file, int lineNumber) { 76 super(c); 77 mFile = file; 78 mLineNumber = lineNumber; 79 } 80 asArray()81 public String[] asArray() { 82 String[] arrayContents = new String[size()]; 83 int i = 0; 84 for (String a : this) { 85 arrayContents[i] = a; 86 i++; 87 } 88 return arrayContents; 89 } 90 getFile()91 public File getFile() { 92 return mFile; 93 } 94 getLineNumber()95 public int getLineNumber() { 96 return mLineNumber; 97 } 98 99 @Override equals(Object o)100 public boolean equals(Object o) { 101 if(o instanceof CommandLine) { 102 CommandLine otherLine = (CommandLine) o; 103 return super.equals(o) && 104 Objects.equals(otherLine.getFile(), mFile) && 105 otherLine.getLineNumber() == mLineNumber; 106 } 107 return false; 108 } 109 110 @Override hashCode()111 public int hashCode() { 112 int listHash = super.hashCode(); 113 return Objects.hash(listHash, mFile, mLineNumber); 114 } 115 } 116 117 /** 118 * Represents a bitmask. Useful because it caches the number of bits which are set. 119 */ 120 static class Bitmask { 121 private List<Boolean> mBitmask = new LinkedList<Boolean>(); 122 private int mNumBitsSet = 0; 123 Bitmask(int nBits)124 public Bitmask(int nBits) { 125 this(nBits, false); 126 } 127 Bitmask(int nBits, boolean initialValue)128 public Bitmask(int nBits, boolean initialValue) { 129 for (int i = 0; i < nBits; ++i) { 130 mBitmask.add(initialValue); 131 } 132 if (initialValue) { 133 mNumBitsSet = nBits; 134 } 135 } 136 137 /** 138 * Return the number of bits which are set (rather than unset) 139 */ getSetCount()140 public int getSetCount() { 141 return mNumBitsSet; 142 } 143 get(int idx)144 public boolean get(int idx) { 145 return mBitmask.get(idx); 146 } 147 set(int idx)148 public boolean set(int idx) { 149 boolean retVal = mBitmask.set(idx, true); 150 if (!retVal) { 151 mNumBitsSet++; 152 } 153 return retVal; 154 } 155 unset(int idx)156 public boolean unset(int idx) { 157 boolean retVal = mBitmask.set(idx, false); 158 if (retVal) { 159 mNumBitsSet--; 160 } 161 return retVal; 162 } 163 remove(int idx)164 public boolean remove(int idx) { 165 boolean retVal = mBitmask.remove(idx); 166 if (retVal) { 167 mNumBitsSet--; 168 } 169 return retVal; 170 } 171 add(int idx, boolean val)172 public void add(int idx, boolean val) { 173 mBitmask.add(idx, val); 174 if (val) { 175 mNumBitsSet++; 176 } 177 } 178 179 /** 180 * Insert a bunch of identical values in the specified spot in the mask 181 * 182 * @param idx the index where the first new value should be set. 183 * @param count the number of new values to insert 184 * @param val the parity of the new values 185 */ addN(int idx, int count, boolean val)186 public void addN(int idx, int count, boolean val) { 187 for (int i = 0; i < count; ++i) { 188 add(idx, val); 189 } 190 } 191 } 192 193 /** 194 * Checks if a line matches the expected format for a (short) macro: 195 * MACRO (name) = (token) [(token)...] 196 * This method verifies that: 197 * <ol> 198 * <li>Line is at least four tokens long</li> 199 * <li>The first token is "MACRO" (case-sensitive)</li> 200 * <li>The third token is an equal-sign</li> 201 * </ol> 202 * 203 * @return {@code true} if the line matches the macro format, {@false} otherwise 204 */ isLineMacro(CommandLine line)205 private static boolean isLineMacro(CommandLine line) { 206 return line.size() >= 4 && "MACRO".equals(line.get(0)) && "=".equals(line.get(2)); 207 } 208 209 /** 210 * Checks if a line matches the expected format for the opening line of a long macro: 211 * LONG MACRO (name) 212 * 213 * @return {@code true} if the line matches the long macro format, {@code false} otherwise 214 */ isLineLongMacro(CommandLine line)215 private static boolean isLineLongMacro(CommandLine line) { 216 return line.size() == 3 && "LONG".equals(line.get(0)) && "MACRO".equals(line.get(1)); 217 } 218 219 /** 220 * Checks if a line matches the expected format for an INCLUDE directive 221 * 222 * @return {@code true} if the line is an INCLUDE directive, {@code false} otherwise 223 */ isLineIncludeDirective(CommandLine line)224 private static boolean isLineIncludeDirective(CommandLine line) { 225 return line.size() == 2 && "INCLUDE".equals(line.get(0)); 226 } 227 228 /** 229 * Checks if a line should be parsed or ignored. Basically, ignore if the line is commented 230 * or is empty. 231 * 232 * @param line A {@link String} containing the line of input to check 233 * @return {@code true} if we should parse the line, {@code false} if we should ignore it. 234 */ shouldParseLine(String line)235 private static boolean shouldParseLine(String line) { 236 line = line.trim(); 237 return !(line.isEmpty() || line.startsWith("#")); 238 } 239 240 /** 241 * Return the command files included by the last parsed command file. 242 */ getIncludedFiles()243 public Collection<String> getIncludedFiles() { 244 return mIncludedFiles; 245 } 246 247 /** 248 * Does a single pass of the input CommandFile, storing input lines as macros, long macros, or 249 * commands. 250 * 251 * Note that this method may call itself recursively to handle the INCLUDE directive. 252 */ scanFile(File file)253 private void scanFile(File file) throws IOException, ConfigurationException { 254 if (mIncludedFiles.contains(file.getAbsolutePath())) { 255 // Repeated include; ignore 256 CLog.v("Skipping repeated include of file %s.", file.toString()); 257 return; 258 } else { 259 mIncludedFiles.add(file.getAbsolutePath()); 260 } 261 262 BufferedReader fileReader = createCommandFileReader(file); 263 String inputLine = null; 264 int lineNumber = 0; 265 try { 266 while ((inputLine = fileReader.readLine()) != null) { 267 lineNumber++; 268 inputLine = inputLine.trim(); 269 if (shouldParseLine(inputLine)) { 270 CommandLine lArgs = null; 271 try { 272 String[] args = QuotationAwareTokenizer.tokenizeLine(inputLine); 273 lArgs = new CommandLine(Arrays.asList(args), file, 274 lineNumber); 275 } catch (IllegalArgumentException e) { 276 throw new ConfigurationException(e.getMessage()); 277 } 278 279 if (isLineMacro(lArgs)) { 280 // Expected format: MACRO <name> = <token> [<token>...] 281 String name = lArgs.get(1); 282 CommandLine expansion = new CommandLine(lArgs.subList(3, lArgs.size()), 283 file, lineNumber); 284 CommandLine prev = mMacros.put(name, expansion); 285 if (prev != null) { 286 CLog.w("Overwrote short macro '%s' while parsing file %s", name, file); 287 CLog.w("value '%s' replaced previous value '%s'", expansion, prev); 288 } 289 } else if (isLineLongMacro(lArgs)) { 290 // Expected format: LONG MACRO <name>\n(multiline expansion)\nEND MACRO 291 String name = lArgs.get(2); 292 List<CommandLine> expansion = new LinkedList<CommandLine>(); 293 294 inputLine = fileReader.readLine(); 295 lineNumber++; 296 while (!"END MACRO".equals(inputLine)) { 297 if (inputLine == null) { 298 // Syntax error 299 throw new ConfigurationException(String.format( 300 "Syntax error: Unexpected EOF while reading definition " + 301 "for LONG MACRO %s.", name)); 302 } 303 if (shouldParseLine(inputLine)) { 304 // Store the tokenized line 305 CommandLine line = new CommandLine(Arrays.asList( 306 QuotationAwareTokenizer.tokenizeLine(inputLine)), 307 file, lineNumber); 308 expansion.add(line); 309 } 310 311 // Advance 312 inputLine = fileReader.readLine(); 313 lineNumber++; 314 } 315 CLog.d("Parsed %d-line definition for long macro %s", expansion.size(), 316 name); 317 318 List<CommandLine> prev = mLongMacros.put(name, expansion); 319 if (prev != null) { 320 CLog.w("Overwrote long macro %s while parsing file %s", name, file); 321 CLog.w("%d-line definition replaced previous %d-line definition", 322 expansion.size(), prev.size()); 323 } 324 } else if (isLineIncludeDirective(lArgs)) { 325 File toScan = new File(lArgs.get(1)); 326 if (toScan.isAbsolute()) { 327 CLog.d("Got an include directive for absolute path %s.", lArgs.get(1)); 328 } else { 329 File parent = file.getParentFile(); 330 toScan = new File(parent, lArgs.get(1)); 331 CLog.d("Got an include directive for relative path %s, using '%s' " + 332 "for parent dir", lArgs.get(1), parent); 333 } 334 scanFile(toScan); 335 } else { 336 mLines.add(lArgs); 337 } 338 } 339 } 340 } finally { 341 fileReader.close(); 342 } 343 } 344 345 /** 346 * Parses the commands contained in {@code file}, doing macro expansions as necessary 347 * 348 * @param file the {@link File} to parse 349 * @return the list of parsed commands 350 * @throws IOException if failed to read file 351 * @throws ConfigurationException if content of file could not be parsed 352 */ parseFile(File file)353 public List<CommandLine> parseFile(File file) throws IOException, 354 ConfigurationException { 355 // clear state from last call 356 mIncludedFiles.clear(); 357 mMacros.clear(); 358 mLongMacros.clear(); 359 mLines.clear(); 360 361 // Parse this cmdfile and all of its dependencies. 362 scanFile(file); 363 364 // remove original file from list of includes, as call above has side effect of adding it to 365 // mIncludedFiles 366 mIncludedFiles.remove(file.getAbsolutePath()); 367 368 // Now perform macro expansion 369 /** 370 * inputBitmask is used to stop iterating when we're sure there are no more macros to 371 * expand. It is a bitmask where the (k)th bit represents the (k)th element in 372 * {@code mLines.} 373 * <p> 374 * Each bit starts as {@code true}, meaning that each line in mLines may have macro calls to 375 * be expanded. We set bits of {@code inputBitmask} to {@code false} once we've determined 376 * that the corresponding lines of {@code mLines} have been fully expanded, which allows us 377 * to skip those lines on subsequent scans. 378 * <p> 379 * {@code inputBitmaskCount} stores the quantity of {@code true} bits in 380 * {@code inputBitmask}. Once {@code inputBitmaskCount == 0}, we are done expanding macros. 381 */ 382 Bitmask inputBitmask = new Bitmask(mLines.size(), true); 383 384 // Do a maximum of 20 iterations of expansion 385 // FIXME: make this configurable 386 for (int iCount = 0; iCount < 20 && inputBitmask.getSetCount() > 0; ++iCount) { 387 CLog.d("### Expansion iteration %d", iCount); 388 389 int inputIdx = 0; 390 while (inputIdx < mLines.size()) { 391 if (!inputBitmask.get(inputIdx)) { 392 // Skip this line; we've already determined that it doesn't contain any macro 393 // calls to be expanded. 394 CLog.d("skipping input line %s", mLines.get(inputIdx)); 395 ++inputIdx; 396 continue; 397 } 398 399 CommandLine line = mLines.get(inputIdx); 400 boolean sawMacro = expandMacro(line); 401 List<CommandLine> longMacroExpansion = expandLongMacro(line, !sawMacro); 402 403 if (longMacroExpansion == null) { 404 if (sawMacro) { 405 // We saw and expanded a short macro. This may have pulled in another macro 406 // to expand, so leave inputBitmask alone. 407 } else { 408 // We did not find any macros (long or short) to expand, thus all expansions 409 // are done for this CommandLine. Update inputBitmask appropriately. 410 inputBitmask.unset(inputIdx); 411 } 412 413 // Finally, advance. 414 ++inputIdx; 415 } else { 416 // We expanded a long macro. First, actually insert the expansion in place of 417 // the macro call 418 mLines.remove(inputIdx); 419 inputBitmask.remove(inputIdx); 420 mLines.addAll(inputIdx, longMacroExpansion); 421 inputBitmask.addN(inputIdx, longMacroExpansion.size(), true); 422 423 // And advance past the end of the expanded macro 424 inputIdx += longMacroExpansion.size(); 425 } 426 } 427 } 428 return mLines; 429 } 430 431 /** 432 * Performs one level of macro expansion for the first macro used in the line 433 */ expandLongMacro(CommandLine line, boolean checkMissingMacro)434 private List<CommandLine> expandLongMacro(CommandLine line, boolean checkMissingMacro) 435 throws ConfigurationException { 436 for (int idx = 0; idx < line.size(); ++idx) { 437 String token = line.get(idx); 438 Matcher matchMacro = MACRO_PATTERN.matcher(token); 439 if (matchMacro.matches()) { 440 // we hit a macro; expand it 441 List<CommandLine> expansion = new LinkedList<CommandLine>(); 442 String name = matchMacro.group(1); 443 List<CommandLine> longMacro = mLongMacros.get(name); 444 if (longMacro == null) { 445 if (checkMissingMacro) { 446 // If the expandMacro method hits an unrecognized macro, it will leave it in 447 // the stream for this method. If it's not recognized here, throw an 448 // exception 449 throw new ConfigurationException(String.format( 450 "Macro call '%s' does not match any macro definitions.", name)); 451 } else { 452 // At this point, it may just be a short macro 453 CLog.d("Macro call '%s' doesn't match any long macro definitions.", name); 454 return null; 455 } 456 } 457 458 LinkedList<String> prefix = new LinkedList<>(line.subList(0, idx)); 459 LinkedList<String> suffix = new LinkedList<>(line.subList(idx, line.size())); 460 suffix.remove(0); 461 for (CommandLine macroLine : longMacro) { 462 CommandLine expanded = new CommandLine(line.getFile(), 463 line.getLineNumber()); 464 expanded.addAll(prefix); 465 expanded.addAll(macroLine); 466 expanded.addAll(suffix); 467 expansion.add(expanded); 468 } 469 470 // Only expand a single macro usage at a time 471 return expansion; 472 } 473 } 474 return null; 475 } 476 477 /** 478 * Performs one level of macro expansion for every macro used in the line 479 * 480 * @return {@code true} if a macro was found and expanded, {@code false} if no macro was found 481 */ expandMacro(CommandLine line)482 private boolean expandMacro(CommandLine line) { 483 boolean sawMacro = false; 484 485 int idx = 0; 486 while (idx < line.size()) { 487 String token = line.get(idx); 488 Matcher matchMacro = MACRO_PATTERN.matcher(token); 489 if (matchMacro.matches() && mMacros.containsKey(matchMacro.group(1))) { 490 // we hit a macro; expand it 491 String name = matchMacro.group(1); 492 CommandLine macro = mMacros.get(name); 493 CLog.d("Gotcha! Expanding macro '%s' to '%s'", name, macro); 494 line.remove(idx); 495 line.addAll(idx, macro); 496 idx += macro.size(); 497 sawMacro = true; 498 } else { 499 ++idx; 500 } 501 } 502 return sawMacro; 503 } 504 505 /** 506 * Create a reader for the command file data. 507 * <p/> 508 * Exposed for unit testing. 509 * 510 * @param file the command {@link File} 511 * @return the {@link BufferedReader} 512 * @throws IOException if failed to read data 513 */ createCommandFileReader(File file)514 BufferedReader createCommandFileReader(File file) throws IOException { 515 return new BufferedReader(new FileReader(file)); 516 } 517 } 518