Make R8 and D8 command-line parsing use only public API

Move command-line parsing to separate classes D8CommandParser and
R8CommandParser.

Move BaseCompilerCommand.parseMinApi() to BaseCompilerCommandParser,
common base class for D8CommandParser and R8CommandParser.

Add BaseCommand.Builder.error() and fatalError() since getReporter() is
not public API. Make FlagFile.expandFlagFiles() accept a Builder instead
of a Reporter.

As a result, {R8,D8}CommandParser can be put in a separate JAR alongside
BaseCompilerCommandParser and FlagFile and linked to a minified R8
library, since they only use the public R8 API.

Also fix a small command-line parsing bug: Give the correct number of
errors when specifying --min-api more than twice.

Change-Id: I0a9a2a174d0b0992a33cf63446573b42111df5e9
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index fbc6ca3..e050356 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -304,6 +304,20 @@
       return self();
     }
 
+    /** Signal an error. */
+    public void error(Diagnostic diagnostic) {
+      reporter.error(diagnostic);
+    }
+
+    /**
+     * Signal an error and throw {@link AbortException}.
+     *
+     * @throws AbortException always.
+     */
+    public RuntimeException fatalError(Diagnostic diagnostic) {
+      return reporter.fatalError(diagnostic);
+    }
+
     // Internal helper for compat tools to make them ignore DEX code in input archives.
     void setIgnoreDexInArchive(boolean value) {
       guard(() -> app.setIgnoreDexInArchive(value));
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 799d834..d724656 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -4,13 +4,11 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.Reporter;
-import com.android.tools.r8.utils.StringDiagnostic;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
@@ -341,31 +339,4 @@
     }
   }
 
-  static <C extends BaseCompilerCommand, B extends BaseCompilerCommand.Builder<C, B>>
-  boolean parseMinApi(
-      BaseCompilerCommand.Builder<C, B> builder,
-      String minApiString,
-      boolean hasDefinedApiLevel,
-      Origin origin) {
-    if (hasDefinedApiLevel) {
-      builder.getReporter().error(new StringDiagnostic(
-          "Cannot set multiple --min-api options", origin));
-      return false;
-    }
-    int minApi;
-    try {
-      minApi = Integer.valueOf(minApiString);
-    } catch (NumberFormatException e) {
-      builder.getReporter().error(new StringDiagnostic(
-          "Invalid argument to --min-api: " + minApiString, origin));
-      return false;
-    }
-    if (minApi < 1) {
-      builder.getReporter().error(new StringDiagnostic(
-          "Invalid argument to --min-api: " + minApiString, origin));
-      return false;
-    }
-    builder.setMinApiLevel(minApi);
-    return true;
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java b/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java
new file mode 100644
index 0000000..054db58
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.StringDiagnostic;
+
+public class BaseCompilerCommandParser {
+
+  static void parseMinApi(BaseCompilerCommand.Builder builder, String minApiString, Origin origin) {
+    int minApi;
+    try {
+      minApi = Integer.valueOf(minApiString);
+    } catch (NumberFormatException e) {
+      builder.error(new StringDiagnostic("Invalid argument to --min-api: " + minApiString, origin));
+      return;
+    }
+    if (minApi < 1) {
+      builder.error(new StringDiagnostic("Invalid argument to --min-api: " + minApiString, origin));
+      return;
+    }
+    builder.setMinApiLevel(minApi);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 6f956db..00d84bd 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -3,18 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
-import com.android.tools.r8.utils.StringDiagnostic;
-import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collection;
 
@@ -150,24 +145,7 @@
     }
   }
 
-  static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
-      "Usage: d8 [options] <input-files>",
-      " where <input-files> are any combination of dex, class, zip, jar, or apk files",
-      " and options are:",
-      "  --debug                 # Compile with debugging information (default).",
-      "  --release               # Compile without debugging information.",
-      "  --output <file>         # Output result in <outfile>.",
-      "                          # <file> must be an existing directory or a zip file.",
-      "  --lib <file>            # Add <file> as a library resource.",
-      "  --classpath <file>      # Add <file> as a classpath resource.",
-      "  --min-api               # Minimum Android API level compatibility",
-      "  --intermediate          # Compile an intermediate result intended for later",
-      "                          # merging.",
-      "  --file-per-class        # Produce a separate dex file per input class",
-      "  --no-desugaring         # Force disable desugaring.",
-      "  --main-dex-list <file>  # List of classes to place in the primary dex file.",
-      "  --version               # Print the version of d8.",
-      "  --help                  # Print this message."));
+  static final String USAGE_MESSAGE = D8CommandParser.USAGE_MESSAGE;
 
   private boolean intermediate = false;
 
@@ -194,7 +172,7 @@
    * @return D8 command builder with state set up according to parsed command line.
    */
   public static Builder parse(String[] args, Origin origin) {
-    return parse(args, origin, builder());
+    return D8CommandParser.parse(args, origin);
   }
 
   /**
@@ -208,87 +186,7 @@
    * @return D8 command builder with state set up according to parsed command line.
    */
   public static Builder parse(String[] args, Origin origin, DiagnosticsHandler handler) {
-    return parse(args, origin, builder(handler));
-  }
-
-  private static Builder parse(String[] args, Origin origin, Builder builder) {
-    CompilationMode compilationMode = null;
-    Path outputPath = null;
-    OutputMode outputMode = null;
-    boolean hasDefinedApiLevel = false;
-    String[] expandedArgs = FlagFile.expandFlagFiles(args, builder.getReporter());
-    try {
-      for (int i = 0; i < expandedArgs.length; i++) {
-        String arg = expandedArgs[i].trim();
-        if (arg.length() == 0) {
-          continue;
-        } else if (arg.equals("--help")) {
-          builder.setPrintHelp(true);
-        } else if (arg.equals("--version")) {
-          builder.setPrintVersion(true);
-        } else if (arg.equals("--debug")) {
-          if (compilationMode == CompilationMode.RELEASE) {
-            builder.getReporter().error(new StringDiagnostic(
-                "Cannot compile in both --debug and --release mode.",
-                origin));
-            continue;
-          }
-          compilationMode = CompilationMode.DEBUG;
-        } else if (arg.equals("--release")) {
-          if (compilationMode == CompilationMode.DEBUG) {
-            builder.getReporter().error(new StringDiagnostic(
-                "Cannot compile in both --debug and --release mode.",
-                origin));
-            continue;
-          }
-          compilationMode = CompilationMode.RELEASE;
-        } else if (arg.equals("--file-per-class")) {
-          outputMode = OutputMode.DexFilePerClassFile;
-        } else if (arg.equals("--output")) {
-          String output = expandedArgs[++i];
-          if (outputPath != null) {
-            builder.getReporter().error(new StringDiagnostic(
-                "Cannot output both to '" + outputPath.toString() + "' and '" + output + "'",
-                origin));
-            continue;
-          }
-          outputPath = Paths.get(output);
-        } else if (arg.equals("--lib")) {
-          builder.addLibraryFiles(Paths.get(expandedArgs[++i]));
-        } else if (arg.equals("--classpath")) {
-          builder.addClasspathFiles(Paths.get(expandedArgs[++i]));
-        } else if (arg.equals("--main-dex-list")) {
-          builder.addMainDexListFiles(Paths.get(expandedArgs[++i]));
-        } else if (arg.equals("--optimize-multidex-for-linearalloc")) {
-          builder.setOptimizeMultidexForLinearAlloc(true);
-        } else if (arg.equals("--min-api")) {
-          hasDefinedApiLevel = parseMinApi(builder, expandedArgs[++i], hasDefinedApiLevel, origin);
-        } else if (arg.equals("--intermediate")) {
-          builder.setIntermediate(true);
-        } else if (arg.equals("--no-desugaring")) {
-          builder.setDisableDesugaring(true);
-        } else {
-          if (arg.startsWith("--")) {
-            builder.getReporter().error(new StringDiagnostic("Unknown option: " + arg,
-                origin));
-            continue;
-          }
-          builder.addProgramFiles(Paths.get(arg));
-        }
-      }
-      if (compilationMode != null) {
-        builder.setMode(compilationMode);
-      }
-      if (outputMode == null) {
-        outputMode = OutputMode.DexIndexed;
-      }
-      if (outputPath == null) {
-        outputPath = Paths.get(".");
-      }
-      return builder.setOutput(outputPath, outputMode);
-    } catch (CompilationError e) {
-      throw builder.getReporter().fatalError(e);
-    }
+    return D8CommandParser.parse(args, origin, handler);
   }
 
   private D8Command(
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
new file mode 100644
index 0000000..9e5d36a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -0,0 +1,157 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.FlagFile;
+import com.android.tools.r8.utils.StringDiagnostic;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+public class D8CommandParser extends BaseCompilerCommandParser {
+
+  public static void main(String[] args) throws CompilationFailedException {
+    D8Command command = parse(args, Origin.root()).build();
+    if (command.isPrintHelp()) {
+      System.out.println(USAGE_MESSAGE);
+      System.exit(1);
+    }
+    D8.run(command);
+  }
+
+  static final String USAGE_MESSAGE =
+      String.join(
+          "\n",
+          Arrays.asList(
+              "Usage: d8 [options] <input-files>",
+              " where <input-files> are any combination of dex, class, zip, jar, or apk files",
+              " and options are:",
+              "  --debug                 # Compile with debugging information (default).",
+              "  --release               # Compile without debugging information.",
+              "  --output <file>         # Output result in <outfile>.",
+              "                          # <file> must be an existing directory or a zip file.",
+              "  --lib <file>            # Add <file> as a library resource.",
+              "  --classpath <file>      # Add <file> as a classpath resource.",
+              "  --min-api               # Minimum Android API level compatibility",
+              "  --intermediate          # Compile an intermediate result intended for later",
+              "                          # merging.",
+              "  --file-per-class        # Produce a separate dex file per input class",
+              "  --no-desugaring         # Force disable desugaring.",
+              "  --main-dex-list <file>  # List of classes to place in the primary dex file.",
+              "  --version               # Print the version of d8.",
+              "  --help                  # Print this message."));
+
+  /**
+   * Parse the D8 command-line.
+   *
+   * <p>Parsing will set the supplied options or their default value if they have any.
+   *
+   * @param args Command-line arguments array.
+   * @param origin Origin description of the command-line arguments.
+   * @return D8 command builder with state set up according to parsed command line.
+   */
+  public static D8Command.Builder parse(String[] args, Origin origin) {
+    return parse(args, origin, D8Command.builder());
+  }
+
+  /**
+   * Parse the D8 command-line.
+   *
+   * <p>Parsing will set the supplied options or their default value if they have any.
+   *
+   * @param args Command-line arguments array.
+   * @param origin Origin description of the command-line arguments.
+   * @param handler Custom defined diagnostics handler.
+   * @return D8 command builder with state set up according to parsed command line.
+   */
+  public static D8Command.Builder parse(String[] args, Origin origin, DiagnosticsHandler handler) {
+    return parse(args, origin, D8Command.builder(handler));
+  }
+
+  private static D8Command.Builder parse(String[] args, Origin origin, D8Command.Builder builder) {
+    CompilationMode compilationMode = null;
+    Path outputPath = null;
+    OutputMode outputMode = null;
+    boolean hasDefinedApiLevel = false;
+    String[] expandedArgs = FlagFile.expandFlagFiles(args, builder);
+    try {
+      for (int i = 0; i < expandedArgs.length; i++) {
+        String arg = expandedArgs[i].trim();
+        if (arg.length() == 0) {
+          continue;
+        } else if (arg.equals("--help")) {
+          builder.setPrintHelp(true);
+        } else if (arg.equals("--version")) {
+          builder.setPrintVersion(true);
+        } else if (arg.equals("--debug")) {
+          if (compilationMode == CompilationMode.RELEASE) {
+            builder.error(
+                new StringDiagnostic("Cannot compile in both --debug and --release mode.", origin));
+            continue;
+          }
+          compilationMode = CompilationMode.DEBUG;
+        } else if (arg.equals("--release")) {
+          if (compilationMode == CompilationMode.DEBUG) {
+            builder.error(
+                new StringDiagnostic("Cannot compile in both --debug and --release mode.", origin));
+            continue;
+          }
+          compilationMode = CompilationMode.RELEASE;
+        } else if (arg.equals("--file-per-class")) {
+          outputMode = OutputMode.DexFilePerClassFile;
+        } else if (arg.equals("--output")) {
+          String output = expandedArgs[++i];
+          if (outputPath != null) {
+            builder.error(
+                new StringDiagnostic(
+                    "Cannot output both to '" + outputPath.toString() + "' and '" + output + "'",
+                    origin));
+            continue;
+          }
+          outputPath = Paths.get(output);
+        } else if (arg.equals("--lib")) {
+          builder.addLibraryFiles(Paths.get(expandedArgs[++i]));
+        } else if (arg.equals("--classpath")) {
+          builder.addClasspathFiles(Paths.get(expandedArgs[++i]));
+        } else if (arg.equals("--main-dex-list")) {
+          builder.addMainDexListFiles(Paths.get(expandedArgs[++i]));
+        } else if (arg.equals("--optimize-multidex-for-linearalloc")) {
+          builder.setOptimizeMultidexForLinearAlloc(true);
+        } else if (arg.equals("--min-api")) {
+          String minApiString = expandedArgs[++i];
+          if (hasDefinedApiLevel) {
+            builder.error(new StringDiagnostic("Cannot set multiple --min-api options", origin));
+          } else {
+            parseMinApi(builder, minApiString, origin);
+            hasDefinedApiLevel = true;
+          }
+        } else if (arg.equals("--intermediate")) {
+          builder.setIntermediate(true);
+        } else if (arg.equals("--no-desugaring")) {
+          builder.setDisableDesugaring(true);
+        } else {
+          if (arg.startsWith("--")) {
+            builder.error(new StringDiagnostic("Unknown option: " + arg, origin));
+            continue;
+          }
+          builder.addProgramFiles(Paths.get(arg));
+        }
+      }
+      if (compilationMode != null) {
+        builder.setMode(compilationMode);
+      }
+      if (outputMode == null) {
+        outputMode = OutputMode.DexIndexed;
+      }
+      if (outputPath == null) {
+        outputPath = Paths.get(".");
+      }
+      return builder.setOutput(outputPath, outputMode);
+    } catch (CompilationError e) {
+      throw builder.fatalError(e);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 573f6df..ad5c882 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -18,7 +18,6 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.Reporter;
@@ -421,38 +420,7 @@
     }
   }
 
-  // Internal state to verify parsing properties not enforced by the builder.
-  private static class ParseState {
-    CompilationMode mode = null;
-    OutputMode outputMode = null;
-    Path outputPath = null;
-    boolean hasDefinedApiLevel = false;
-  }
-
-  static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
-      "Usage: r8 [options] <input-files>",
-      " where <input-files> are any combination of dex, class, zip, jar, or apk files",
-      " and options are:",
-      "  --release                # Compile without debugging information (default).",
-      "  --debug                  # Compile with debugging information.",
-      // TODO(b/65390962): Add help for output-mode flags once the CF backend is complete.
-      //"  --dex                    # Compile program to DEX file format (default).",
-      //"  --classfile              # Compile program to Java classfile format.",
-      "  --output <file>          # Output result in <file>.",
-      "                           # <file> must be an existing directory or a zip file.",
-      "  --lib <file>             # Add <file> as a library resource.",
-      "  --min-api                # Minimum Android API level compatibility.",
-      "  --pg-conf <file>         # Proguard configuration <file>.",
-      "  --pg-map-output <file>   # Output the resulting name and line mapping to <file>.",
-      "  --no-tree-shaking        # Force disable tree shaking of unreachable classes.",
-      "  --no-minification        # Force disable minification of names.",
-      "  --no-desugaring          # Force disable desugaring.",
-      "  --main-dex-rules <file>  # Proguard keep rules for classes to place in the",
-      "                           # primary dex file.",
-      "  --main-dex-list <file>   # List of classes to place in the primary dex file.",
-      "  --main-dex-list-output <file>  # Output the full main-dex list in <file>.",
-      "  --version                # Print the version of r8.",
-      "  --help                   # Print this message."));
+  static final String USAGE_MESSAGE = R8CommandParser.USAGE_MESSAGE;
 
   private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
   private final StringConsumer mainDexListConsumer;
@@ -488,7 +456,7 @@
    * @return R8 command builder with state set up according to parsed command line.
    */
   public static Builder parse(String[] args, Origin origin) {
-    return parse(args, origin, builder());
+    return R8CommandParser.parse(args, origin);
   }
 
   /**
@@ -502,102 +470,7 @@
    * @return R8 command builder with state set up according to parsed command line.
    */
   public static Builder parse(String[] args, Origin origin, DiagnosticsHandler handler) {
-    return parse(args, origin, builder(handler));
-  }
-
-  private static Builder parse(String[] args, Origin origin, Builder builder) {
-    ParseState state = new ParseState();
-    parse(args, origin, builder, state);
-    if (state.mode != null) {
-      builder.setMode(state.mode);
-    }
-    Path outputPath = state.outputPath != null ? state.outputPath : Paths.get(".");
-    OutputMode outputMode = state.outputMode != null ? state.outputMode : OutputMode.DexIndexed;
-    builder.setOutput(outputPath, outputMode);
-    return builder;
-  }
-
-  private static ParseState parse(
-      String[] args,
-      Origin argsOrigin,
-      Builder builder,
-      ParseState state) {
-    String[] expandedArgs = FlagFile.expandFlagFiles(args, builder.getReporter());
-    for (int i = 0; i < expandedArgs.length; i++) {
-      String arg = expandedArgs[i].trim();
-      if (arg.length() == 0) {
-        continue;
-      } else if (arg.equals("--help")) {
-        builder.setPrintHelp(true);
-      } else if (arg.equals("--version")) {
-        builder.setPrintVersion(true);
-      } else if (arg.equals("--debug")) {
-        if (state.mode == CompilationMode.RELEASE) {
-          builder.getReporter().error(new StringDiagnostic(
-              "Cannot compile in both --debug and --release mode.", argsOrigin));
-        }
-        state.mode = CompilationMode.DEBUG;
-      } else if (arg.equals("--release")) {
-        if (state.mode == CompilationMode.DEBUG) {
-          builder.getReporter().error(new StringDiagnostic(
-              "Cannot compile in both --debug and --release mode.", argsOrigin));
-        }
-        state.mode = CompilationMode.RELEASE;
-      } else if (arg.equals("--dex")) {
-        if (state.outputMode == OutputMode.ClassFile) {
-          builder.getReporter().error(new StringDiagnostic(
-              "Cannot compile in both --dex and --classfile output mode.", argsOrigin));
-        }
-        state.outputMode = OutputMode.DexIndexed;
-      } else if (arg.equals("--classfile")) {
-        if (state.outputMode == OutputMode.DexIndexed) {
-          builder.getReporter().error(new StringDiagnostic(
-              "Cannot compile in both --dex and --classfile output mode.", argsOrigin));
-        }
-        state.outputMode = OutputMode.ClassFile;
-      } else if (arg.equals("--output")) {
-        String outputPath = expandedArgs[++i];
-        if (state.outputPath != null) {
-          builder.getReporter().error(new StringDiagnostic(
-              "Cannot output both to '"
-                  + state.outputPath.toString()
-                  + "' and '"
-                  + outputPath
-                  + "'",
-              argsOrigin));
-        }
-        state.outputPath = Paths.get(outputPath);
-      } else if (arg.equals("--lib")) {
-        builder.addLibraryFiles(Paths.get(expandedArgs[++i]));
-      } else if (arg.equals("--min-api")) {
-        state.hasDefinedApiLevel =
-            parseMinApi(builder, expandedArgs[++i], state.hasDefinedApiLevel, argsOrigin);
-      } else if (arg.equals("--no-tree-shaking")) {
-        builder.setDisableTreeShaking(true);
-      } else if (arg.equals("--no-minification")) {
-        builder.setDisableMinification(true);
-      } else if (arg.equals("--no-desugaring")) {
-        builder.setDisableDesugaring(true);
-      } else if (arg.equals("--main-dex-rules")) {
-        builder.addMainDexRulesFiles(Paths.get(expandedArgs[++i]));
-      } else if (arg.equals("--main-dex-list")) {
-        builder.addMainDexListFiles(Paths.get(expandedArgs[++i]));
-      } else if (arg.equals("--main-dex-list-output")) {
-        builder.setMainDexListOutputPath(Paths.get(expandedArgs[++i]));
-      } else if (arg.equals("--optimize-multidex-for-linearalloc")) {
-        builder.setOptimizeMultidexForLinearAlloc(true);
-      } else if (arg.equals("--pg-conf")) {
-        builder.addProguardConfigurationFiles(Paths.get(expandedArgs[++i]));
-      } else if (arg.equals("--pg-map-output")) {
-        builder.setProguardMapOutputPath(Paths.get(expandedArgs[++i]));
-      } else {
-        if (arg.startsWith("--")) {
-          builder.getReporter().error(new StringDiagnostic("Unknown option: " + arg, argsOrigin));
-        }
-        builder.addProgramFiles(Paths.get(arg));
-      }
-    }
-    return state;
+    return R8CommandParser.parse(args, origin, handler);
   }
 
   private R8Command(
diff --git a/src/main/java/com/android/tools/r8/R8CommandParser.java b/src/main/java/com/android/tools/r8/R8CommandParser.java
new file mode 100644
index 0000000..3d33037
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/R8CommandParser.java
@@ -0,0 +1,186 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.FlagFile;
+import com.android.tools.r8.utils.StringDiagnostic;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+public class R8CommandParser extends BaseCompilerCommandParser {
+
+  public static void main(String[] args) throws CompilationFailedException {
+    R8Command command = parse(args, Origin.root()).build();
+    if (command.isPrintHelp()) {
+      System.out.println(USAGE_MESSAGE);
+      System.exit(1);
+    }
+    R8.run(command);
+  }
+
+  // Internal state to verify parsing properties not enforced by the builder.
+  private static class ParseState {
+    CompilationMode mode = null;
+    OutputMode outputMode = null;
+    Path outputPath = null;
+    boolean hasDefinedApiLevel = false;
+  }
+
+  static final String USAGE_MESSAGE =
+      String.join(
+          "\n",
+          Arrays.asList(
+              "Usage: r8 [options] <input-files>",
+              " where <input-files> are any combination of dex, class, zip, jar, or apk files",
+              " and options are:",
+              "  --release                # Compile without debugging information (default).",
+              "  --debug                  # Compile with debugging information.",
+              // TODO(b/65390962): Add help for output-mode flags once the CF backend is complete.
+              // "  --dex                    # Compile program to DEX file format (default).",
+              // "  --classfile              # Compile program to Java classfile format.",
+              "  --output <file>          # Output result in <file>.",
+              "                           # <file> must be an existing directory or a zip file.",
+              "  --lib <file>             # Add <file> as a library resource.",
+              "  --min-api                # Minimum Android API level compatibility.",
+              "  --pg-conf <file>         # Proguard configuration <file>.",
+              "  --pg-map-output <file>   # Output the resulting name and line mapping to <file>.",
+              "  --no-tree-shaking        # Force disable tree shaking of unreachable classes.",
+              "  --no-minification        # Force disable minification of names.",
+              "  --no-desugaring          # Force disable desugaring.",
+              "  --main-dex-rules <file>  # Proguard keep rules for classes to place in the",
+              "                           # primary dex file.",
+              "  --main-dex-list <file>   # List of classes to place in the primary dex file.",
+              "  --main-dex-list-output <file>  # Output the full main-dex list in <file>.",
+              "  --version                # Print the version of r8.",
+              "  --help                   # Print this message."));
+  /**
+   * Parse the R8 command-line.
+   *
+   * <p>Parsing will set the supplied options or their default value if they have any.
+   *
+   * @param args Command-line arguments array.
+   * @param origin Origin description of the command-line arguments.
+   * @return R8 command builder with state set up according to parsed command line.
+   */
+  public static R8Command.Builder parse(String[] args, Origin origin) {
+    return new R8CommandParser().parse(args, origin, R8Command.builder());
+  }
+
+  /**
+   * Parse the R8 command-line.
+   *
+   * <p>Parsing will set the supplied options or their default value if they have any.
+   *
+   * @param args Command-line arguments array.
+   * @param origin Origin description of the command-line arguments.
+   * @param handler Custom defined diagnostics handler.
+   * @return R8 command builder with state set up according to parsed command line.
+   */
+  public static R8Command.Builder parse(String[] args, Origin origin, DiagnosticsHandler handler) {
+    return new R8CommandParser().parse(args, origin, R8Command.builder(handler));
+  }
+
+  private R8Command.Builder parse(String[] args, Origin origin, R8Command.Builder builder) {
+    ParseState state = new ParseState();
+    parse(args, origin, builder, state);
+    if (state.mode != null) {
+      builder.setMode(state.mode);
+    }
+    Path outputPath = state.outputPath != null ? state.outputPath : Paths.get(".");
+    OutputMode outputMode = state.outputMode != null ? state.outputMode : OutputMode.DexIndexed;
+    builder.setOutput(outputPath, outputMode);
+    return builder;
+  }
+
+  private void parse(
+      String[] args, Origin argsOrigin, R8Command.Builder builder, ParseState state) {
+    String[] expandedArgs = FlagFile.expandFlagFiles(args, builder);
+    for (int i = 0; i < expandedArgs.length; i++) {
+      String arg = expandedArgs[i].trim();
+      if (arg.length() == 0) {
+        continue;
+      } else if (arg.equals("--help")) {
+        builder.setPrintHelp(true);
+      } else if (arg.equals("--version")) {
+        builder.setPrintVersion(true);
+      } else if (arg.equals("--debug")) {
+        if (state.mode == CompilationMode.RELEASE) {
+          builder.error(
+              new StringDiagnostic(
+                  "Cannot compile in both --debug and --release mode.", argsOrigin));
+        }
+        state.mode = CompilationMode.DEBUG;
+      } else if (arg.equals("--release")) {
+        if (state.mode == CompilationMode.DEBUG) {
+          builder.error(
+              new StringDiagnostic(
+                  "Cannot compile in both --debug and --release mode.", argsOrigin));
+        }
+        state.mode = CompilationMode.RELEASE;
+      } else if (arg.equals("--dex")) {
+        if (state.outputMode == OutputMode.ClassFile) {
+          builder.error(
+              new StringDiagnostic(
+                  "Cannot compile in both --dex and --classfile output mode.", argsOrigin));
+        }
+        state.outputMode = OutputMode.DexIndexed;
+      } else if (arg.equals("--classfile")) {
+        if (state.outputMode == OutputMode.DexIndexed) {
+          builder.error(
+              new StringDiagnostic(
+                  "Cannot compile in both --dex and --classfile output mode.", argsOrigin));
+        }
+        state.outputMode = OutputMode.ClassFile;
+      } else if (arg.equals("--output")) {
+        String outputPath = expandedArgs[++i];
+        if (state.outputPath != null) {
+          builder.error(
+              new StringDiagnostic(
+                  "Cannot output both to '"
+                      + state.outputPath.toString()
+                      + "' and '"
+                      + outputPath
+                      + "'",
+                  argsOrigin));
+        }
+        state.outputPath = Paths.get(outputPath);
+      } else if (arg.equals("--lib")) {
+        builder.addLibraryFiles(Paths.get(expandedArgs[++i]));
+      } else if (arg.equals("--min-api")) {
+        String minApiString = expandedArgs[++i];
+        if (state.hasDefinedApiLevel) {
+          builder.error(new StringDiagnostic("Cannot set multiple --min-api options", argsOrigin));
+        } else {
+          parseMinApi(builder, minApiString, argsOrigin);
+          state.hasDefinedApiLevel = true;
+        }
+      } else if (arg.equals("--no-tree-shaking")) {
+        builder.setDisableTreeShaking(true);
+      } else if (arg.equals("--no-minification")) {
+        builder.setDisableMinification(true);
+      } else if (arg.equals("--no-desugaring")) {
+        builder.setDisableDesugaring(true);
+      } else if (arg.equals("--main-dex-rules")) {
+        builder.addMainDexRulesFiles(Paths.get(expandedArgs[++i]));
+      } else if (arg.equals("--main-dex-list")) {
+        builder.addMainDexListFiles(Paths.get(expandedArgs[++i]));
+      } else if (arg.equals("--main-dex-list-output")) {
+        builder.setMainDexListOutputPath(Paths.get(expandedArgs[++i]));
+      } else if (arg.equals("--optimize-multidex-for-linearalloc")) {
+        builder.setOptimizeMultidexForLinearAlloc(true);
+      } else if (arg.equals("--pg-conf")) {
+        builder.addProguardConfigurationFiles(Paths.get(expandedArgs[++i]));
+      } else if (arg.equals("--pg-map-output")) {
+        builder.setProguardMapOutputPath(Paths.get(expandedArgs[++i]));
+      } else {
+        if (arg.startsWith("--")) {
+          builder.error(new StringDiagnostic("Unknown option: " + arg, argsOrigin));
+        }
+        builder.addProgramFiles(Paths.get(arg));
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/FlagFile.java b/src/main/java/com/android/tools/r8/utils/FlagFile.java
index 4791217..5f5003f 100644
--- a/src/main/java/com/android/tools/r8/utils/FlagFile.java
+++ b/src/main/java/com/android/tools/r8/utils/FlagFile.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.BaseCommand;
 import com.android.tools.r8.origin.Origin;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -28,7 +29,7 @@
     }
   }
 
-  public static String[] expandFlagFiles(String[] args, Reporter reporter) {
+  public static String[] expandFlagFiles(String[] args, BaseCommand.Builder builder) {
     List<String> flags = new ArrayList<>(args.length);
     for (String arg : args) {
       if (arg.startsWith("@")) {
@@ -37,7 +38,7 @@
           flags.addAll(Files.readAllLines(flagFilePath));
         } catch (IOException e) {
           Origin origin = new FlagFileOrigin(flagFilePath);
-          reporter.error(new ExceptionDiagnostic(e, origin));
+          builder.error(new ExceptionDiagnostic(e, origin));
         }
       } else {
         flags.add(arg);