Home | History | Annotate | Download | only in command
      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 
     69         CommandLine(File file, int lineNumber) {
     70             super();
     71             mFile = file;
     72             mLineNumber = lineNumber;
     73         }
     74 
     75         CommandLine(Collection<? extends String> c, File file, int lineNumber) {
     76             super(c);
     77             mFile = file;
     78             mLineNumber = lineNumber;
     79         }
     80 
     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 
     91         public File getFile() {
     92             return mFile;
     93         }
     94 
     95         public int getLineNumber() {
     96             return mLineNumber;
     97         }
     98 
     99         @Override
    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
    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 
    124         public Bitmask(int nBits) {
    125             this(nBits, false);
    126         }
    127 
    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          */
    140         public int getSetCount() {
    141             return mNumBitsSet;
    142         }
    143 
    144         public boolean get(int idx) {
    145             return mBitmask.get(idx);
    146         }
    147 
    148         public boolean set(int idx) {
    149             boolean retVal = mBitmask.set(idx, true);
    150             if (!retVal) {
    151                 mNumBitsSet++;
    152             }
    153             return retVal;
    154         }
    155 
    156         public boolean unset(int idx) {
    157             boolean retVal = mBitmask.set(idx, false);
    158             if (retVal) {
    159                 mNumBitsSet--;
    160             }
    161             return retVal;
    162         }
    163 
    164         public boolean remove(int idx) {
    165             boolean retVal = mBitmask.remove(idx);
    166             if (retVal) {
    167                 mNumBitsSet--;
    168             }
    169             return retVal;
    170         }
    171 
    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          */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    514     BufferedReader createCommandFileReader(File file) throws IOException {
    515         return new BufferedReader(new FileReader(file));
    516     }
    517 }
    518