Share --output in CliParserUtils and improve descriptions.

Change-Id: I9d8fecaf3fa58200bbf98039b77b162c01bb34e9
diff --git a/src/main/java/com/android/tools/r8/BackportedMethodListCommand.java b/src/main/java/com/android/tools/r8/BackportedMethodListCommand.java
index 2875d1f..d340494 100644
--- a/src/main/java/com/android/tools/r8/BackportedMethodListCommand.java
+++ b/src/main/java/com/android/tools/r8/BackportedMethodListCommand.java
@@ -131,15 +131,20 @@
 
   private static CliParser<Builder> createParser() {
     BooleanBox hasDefinedApiLevel = new BooleanBox(false);
+    BooleanBox hasSetOutputPath = new BooleanBox(false);
     var parser =
         new CliParser<Builder>(
             StringUtils.lines("Usage: BackportedMethodList [options]", " Options are:"));
     parser
-        .option1(
-            "--output",
-            "<file>",
-            "Output result in <file>.",
-            (b, arg) -> b.setOutputPath(Paths.get(arg)))
+        .apply(
+            CliParserUtils.addOutputOption(
+                b -> hasSetOutputPath.get(),
+                (b, path) -> {
+                  b.setOutputPath(path);
+                  hasSetOutputPath.set(true);
+                },
+                (b, error) -> b.reporter.error(new StringDiagnostic(error)),
+                CliParserUtils.OutputOptionType.FILE))
         .apply(
             CliParserUtils.addMinApiOption(
                 b -> hasDefinedApiLevel.get(),
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
index ce5ffc8..94fc092 100644
--- a/src/main/java/com/android/tools/r8/D8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -176,20 +176,12 @@
                 state.compilationMode = CompilationMode.RELEASE;
               }
             })
-        .option1(
-            "--output",
-            "<file>",
-            "Output result in <file>. <file> must be an existing directory or a zip file.",
-            (state, arg) -> {
-              if (state.outputPath != null) {
-                state.builder.error(
-                    new StringDiagnostic(
-                        "Cannot output both to '" + state.outputPath + "' and '" + arg + "'",
-                        state.origin));
-              } else {
-                state.outputPath = Paths.get(arg);
-              }
-            })
+        .apply(
+            CliParserUtils.addOutputOption(
+                state -> state.outputPath != null,
+                (state, path) -> state.outputPath = path,
+                (state, error) -> state.builder.error(new StringDiagnostic(error, state.origin)),
+                CliParserUtils.OutputOptionType.ZIP_OR_EXISTING_DIR))
         .option1(
             "--globals",
             "<file>",
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParser.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParser.java
index 92de03b..527c631 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParser.java
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParser.java
@@ -65,20 +65,12 @@
             "<file|jdk-home>",
             "Add <file|jdk-home> as a library resource.",
             (b, arg) -> b.builder.addLibraryFiles(Paths.get(arg)))
-        .option1(
-            "--output",
-            "<globals-file>",
-            "Output result in <globals-file>.",
-            (b, arg) -> {
-              if (b.outputPath != null) {
-                StringDiagnostic diagnostic =
-                    new StringDiagnostic(
-                        "Cannot output both to '" + b.outputPath + "' and '" + arg + "'", b.origin);
-                b.builder.error(diagnostic);
-              } else {
-                b.outputPath = Paths.get(arg);
-              }
-            })
+        .apply(
+            CliParserUtils.addOutputOption(
+                b -> b.outputPath != null,
+                (b, path) -> b.outputPath = path,
+                (b, error) -> b.builder.error(new StringDiagnostic(error, b.origin)),
+                CliParserUtils.OutputOptionType.FILE))
         .option0(
             "--classfile",
             "Generate globals for only classfile to classfile desugaring. (By default globals for"
diff --git a/src/main/java/com/android/tools/r8/L8CommandParser.java b/src/main/java/com/android/tools/r8/L8CommandParser.java
index 7ea71a6..e2b989d 100644
--- a/src/main/java/com/android/tools/r8/L8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/L8CommandParser.java
@@ -96,21 +96,12 @@
                 state.compilationMode = CompilationMode.RELEASE;
               }
             })
-        .option1(
-            "--output",
-            "<file>",
-            "Output result in <file>. <file> must be an existing directory or a zip file.",
-            (state, arg) -> {
-              if (state.outputPath != null) {
-                StringDiagnostic diagnostic =
-                    new StringDiagnostic(
-                        "Cannot output both to '" + state.outputPath + "' and '" + arg + "'",
-                        state.origin);
-                state.builder.error(diagnostic);
-              } else {
-                state.outputPath = Paths.get(arg);
-              }
-            })
+        .apply(
+            CliParserUtils.addOutputOption(
+                state -> state.outputPath != null,
+                (state, path) -> state.outputPath = path,
+                (state, error) -> state.builder.error(new StringDiagnostic(error, state.origin)),
+                CliParserUtils.OutputOptionType.ZIP_OR_EXISTING_DIR))
         .option1(
             "--lib",
             "<file|jdk-home>",
diff --git a/src/main/java/com/android/tools/r8/R8CommandParser.java b/src/main/java/com/android/tools/r8/R8CommandParser.java
index 8a0d042..c491411 100644
--- a/src/main/java/com/android/tools/r8/R8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/R8CommandParser.java
@@ -95,20 +95,12 @@
               }
               state.outputMode = OutputMode.ClassFile;
             })
-        .option1(
-            "--output",
-            "<file>",
-            "Output result in <file>. <file> must be an existing directory or a zip file.",
-            (state, arg) -> {
-              if (state.outputPath != null) {
-                StringDiagnostic diagnostic =
-                    new StringDiagnostic(
-                        "Cannot output both to '" + state.outputPath + "' and '" + arg + "'",
-                        state.origin);
-                state.builder.error(diagnostic);
-              }
-              state.outputPath = Paths.get(arg);
-            })
+        .apply(
+            CliParserUtils.addOutputOption(
+                state -> state.outputPath != null,
+                (state, path) -> state.outputPath = path,
+                (state, error) -> state.builder.error(new StringDiagnostic(error, state.origin)),
+                CliParserUtils.OutputOptionType.ZIP_OR_EXISTING_DIR))
         .option1(
             "--lib",
             "<file|jdk-home>",
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommand.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommand.java
index 6a00d8d..9744154 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommand.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommand.java
@@ -267,6 +267,7 @@
     private static class ParserState {
       final DesugaredMethodsListCommand.Builder builder;
       boolean hasDefinedApiLevel = false;
+      Path outputPath = null;
 
       ParserState(DesugaredMethodsListCommand.Builder builder) {
         this.builder = builder;
@@ -277,11 +278,15 @@
       var header = "Usage: desugaredmethods [options] where  options are:";
       var parser = new CliParser<ParserState>(header);
       return parser
-          .option1(
-              "--output",
-              "<file>",
-              "Output result in <file>. <file> must be an existing directory or a zip file.",
-              (state, arg) -> state.builder.setOutputPath(Paths.get(arg)))
+          .apply(
+              CliParserUtils.addOutputOption(
+                  state -> state.outputPath != null,
+                  (state, path) -> {
+                    state.outputPath = path;
+                    state.builder.setOutputPath(path);
+                  },
+                  (state, error) -> state.builder.reporter.error(new StringDiagnostic(error)),
+                  CliParserUtils.OutputOptionType.FILE))
           .option1(
               "--lib",
               "<file|jdk-home>",
diff --git a/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java b/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
index 77d1c31..f00d7d6 100644
--- a/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
+++ b/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
@@ -430,20 +430,12 @@
                           s.origin));
                 }
               })
-          .option1(
-              "--output",
-              "<file>",
-              "Output result in <outfile>.",
-              (s, arg) -> {
-                if (s.outputPath != null) {
-                  s.builder.error(
-                      new StringDiagnostic(
-                          "Cannot output both to '" + s.outputPath + "' and '" + arg + "'",
-                          s.origin));
-                } else {
-                  s.outputPath = Paths.get(arg);
-                }
-              })
+          .apply(
+              CliParserUtils.addOutputOption(
+                  s -> s.outputPath != null,
+                  (s, path) -> s.outputPath = path,
+                  (s, error) -> s.builder.error(new StringDiagnostic(error, s.origin)),
+                  CliParserUtils.OutputOptionType.ZIP_OR_EXISTING_DIR))
           .option1(
               "--map",
               "<from->to>",
diff --git a/src/main/java/com/android/tools/r8/utils/CliParserUtils.java b/src/main/java/com/android/tools/r8/utils/CliParserUtils.java
index 869f697..eed8ebf 100644
--- a/src/main/java/com/android/tools/r8/utils/CliParserUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/CliParserUtils.java
@@ -22,6 +22,8 @@
 import com.android.tools.r8.utils.internal.collections.Pair;
 import com.android.tools.r8.utils.internal.exceptions.Unreachable;
 import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.BiConsumer;
@@ -329,6 +331,39 @@
             });
   }
 
+  public enum OutputOptionType {
+    ZIP_OR_EXISTING_DIR(
+        "<path>", "Output result in <path>. <path> must be a zip file or an existing directory."),
+    FILE("<file>", "Output result in <file>.");
+
+    private final String paramLabel;
+    private final String description;
+
+    OutputOptionType(String paramLabel, String description) {
+      this.paramLabel = paramLabel;
+      this.description = description;
+    }
+  }
+
+  public static <B> Consumer<CliParser<B>> addOutputOption(
+      Predicate<B> hasSetOutputPathAlready,
+      BiConsumer<B, Path> action,
+      BiConsumer<B, String> errorHandler,
+      OutputOptionType outputOptionType) {
+    return parser ->
+        parser.option1(
+            "--output",
+            outputOptionType.paramLabel,
+            outputOptionType.description,
+            (state, arg) -> {
+              if (hasSetOutputPathAlready.test(state)) {
+                errorHandler.accept(state, "Cannot set multiple --output options");
+              } else {
+                action.accept(state, Paths.get(arg));
+              }
+            });
+  }
+
   public static <B> Consumer<CliParser<B>> addThreadCountOption(
       BiConsumer<B, Integer> action,
       Function<B, ? extends DiagnosticsHandler> getDiagnosticsHandler,
diff --git a/src/test/java/com/android/tools/r8/D8CommandTest.java b/src/test/java/com/android/tools/r8/D8CommandTest.java
index f019b19..fdad625 100644
--- a/src/test/java/com/android/tools/r8/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/D8CommandTest.java
@@ -177,9 +177,9 @@
             " and options are:",
             "  --debug                 # Compile with debugging information (default).",
             "  --release               # Compile without debugging information.",
-            "  --output <file>         # Output result in <file>. <file> must be an existing"
-                + " directory or a zip",
-            "                          # file.",
+            "  --output <path>         # Output result in <path>. <path> must be a zip file or"
+                + " an existing",
+            "                          # directory.",
             "  --globals <file>        # Global synthetics <file> from a previous intermediate"
                 + " compilation. The",
             "                          # <file> may be either a zip-archive of global"
diff --git a/src/test/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParserTest.java b/src/test/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParserTest.java
index 927ef37..89e087a 100644
--- a/src/test/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParserTest.java
+++ b/src/test/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParserTest.java
@@ -35,7 +35,7 @@
             "  --min-api <number>[.<number>]",
             "                          # Minimum Android API level compatibility (default: 1).",
             "  --lib <file|jdk-home>   # Add <file|jdk-home> as a library resource.",
-            "  --output <globals-file> # Output result in <globals-file>.",
+            "  --output <file>         # Output result in <file>.",
             "  --classfile             # Generate globals for only classfile to classfile"
                 + " desugaring. (By default",
             "                          # globals for both classfile and dex desugaring are"
diff --git a/src/test/java/com/android/tools/r8/L8CommandTest.java b/src/test/java/com/android/tools/r8/L8CommandTest.java
index a61a4d8..2cd05be 100644
--- a/src/test/java/com/android/tools/r8/L8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/L8CommandTest.java
@@ -95,9 +95,9 @@
             " and options are:",
             "  --debug                 # Compile with debugging information (default).",
             "  --release               # Compile without debugging information.",
-            "  --output <file>         # Output result in <file>. <file> must be an existing"
-                + " directory or a zip",
-            "                          # file.",
+            "  --output <path>         # Output result in <path>. <path> must be a zip file or"
+                + " an existing",
+            "                          # directory.",
             "  --lib <file|jdk-home>   # Add <file|jdk-home> as a library resource.",
             "  --min-api <number>[.<number>]",
             "                          # Minimum Android API level compatibility (default: 1).",
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index 2a6d280..c5648dd 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -371,9 +371,9 @@
             "  --debug                 # Compile with debugging information.",
             "  --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.",
+            "  --output <path>         # Output result in <path>. <path> must be a zip file or"
+                + " an existing",
+            "                          # directory.",
             "  --lib <file|jdk-home>   # Add <file|jdk-home> as a library resource.",
             "  --classpath <file>      # Add <file> as a classpath resource.",
             "  --min-api <number>[.<number>]",
diff --git a/src/test/java/com/android/tools/r8/relocator/RelocatorCommandTest.java b/src/test/java/com/android/tools/r8/relocator/RelocatorCommandTest.java
index 56571aa..da2968b 100644
--- a/src/test/java/com/android/tools/r8/relocator/RelocatorCommandTest.java
+++ b/src/test/java/com/android/tools/r8/relocator/RelocatorCommandTest.java
@@ -56,7 +56,9 @@
             "  --input <file>          # Input file to remap, class, zip or jar.",
             "  --input-no-res <file>   # Input file to remap, zip or jar. Only .class file entries"
                 + " are included.",
-            "  --output <file>         # Output result in <outfile>.",
+            "  --output <path>         # Output result in <path>. <path> must be a zip file or"
+                + " an existing",
+            "                          # directory.",
             "  --map <from->to>        # Registers a mapping.",
             "  --map-diagnostics[:<type>] <from-level> <to-level>",
             "                          # Map diagnostics of <type> (default any) reported as"
@@ -170,8 +172,7 @@
                   .build();
             });
     assertThat(
-        exception.getCause().getMessage(),
-        containsString("Cannot output both to 'first_output.zip' and 'another_output.zip'"));
+        exception.getCause().getMessage(), containsString("Cannot set multiple --output options"));
   }
 
   @Test
diff --git a/src/test/java8/ir/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommandTest.java b/src/test/java8/ir/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommandTest.java
index e3ccc98..b250623 100644
--- a/src/test/java8/ir/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommandTest.java
+++ b/src/test/java8/ir/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommandTest.java
@@ -40,9 +40,7 @@
     assertEquals(
         StringUtils.lines(
             "Usage: desugaredmethods [options] where  options are:",
-            "  --output <file>         # Output result in <file>. <file> must be an existing"
-                + " directory or a zip",
-            "                          # file.",
+            "  --output <file>         # Output result in <file>.",
             "  --lib <file|jdk-home>   # Add <file|jdk-home> as a library resource.",
             "  --min-api <number>[.<number>]",
             "                          # Minimum Android API level compatibility (default: 1).",