Add support for @argsfile

Summary:
added a new argument type with the format @[filename].
If supplied, ktfmt arguments will be extracted from file.

Reviewed By: cgrushko

Differential Revision: D34612728

fbshipit-source-id: 46cb237b12fd929719fcab7c05a5f42ec7d398fa
diff --git a/core/src/main/java/com/facebook/ktfmt/cli/Main.kt b/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
index f71cd4c..4c965bb 100644
--- a/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
+++ b/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
@@ -65,12 +65,13 @@
     }
   }
 
-  private val parsedArgs: ParsedArgs = ParsedArgs.parseOptions(err, args)
+  private val parsedArgs: ParsedArgs = ParsedArgs.processArgs(err, args)
 
   fun run(): Int {
     if (parsedArgs.fileNames.isEmpty()) {
       err.println(
           "Usage: ktfmt [--dropbox-style | --google-style | --kotlinlang-style] [--dry-run] [--set-exit-if-changed] File1.kt File2.kt ...")
+      err.println("Or: ktfmt @file")
       return 1
     }
 
diff --git a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
index bb959c8..503cb89 100644
--- a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
+++ b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
@@ -18,6 +18,7 @@
 
 import com.facebook.ktfmt.format.Formatter
 import com.facebook.ktfmt.format.FormattingOptions
+import java.io.File
 import java.io.PrintStream
 
 /** ParsedArgs holds the arguments passed to ktfmt on the command-line, after parsing. */
@@ -34,6 +35,15 @@
     val setExitIfChanged: Boolean,
 ) {
   companion object {
+
+    fun processArgs(err: PrintStream, args: Array<String>): ParsedArgs {
+      if (args.size == 1 && args[0].startsWith("@")) {
+        return parseOptions(err, File(args[0].substring(1)).readLines().toTypedArray())
+      } else {
+        return parseOptions(err, args)
+      }
+    }
+
     /** parseOptions parses command-line arguments passed to ktfmt. */
     fun parseOptions(err: PrintStream, args: Array<String>): ParsedArgs {
       val fileNames = mutableListOf<String>()
@@ -49,6 +59,7 @@
           arg == "--dry-run" || arg == "-n" -> dryRun = true
           arg == "--set-exit-if-changed" -> setExitIfChanged = true
           arg.startsWith("--") -> err.println("Unexpected option: $arg")
+          arg.startsWith("@") -> err.println("Unexpected option: $arg")
           else -> fileNames.add(arg)
         }
       }
diff --git a/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt b/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt
index 75df85a..63d4c2f 100644
--- a/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt
@@ -20,7 +20,10 @@
 import com.facebook.ktfmt.format.FormattingOptions
 import com.google.common.truth.Truth.assertThat
 import java.io.ByteArrayOutputStream
+import java.io.FileNotFoundException
 import java.io.PrintStream
+import junit.framework.Assert.fail
+import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -29,6 +32,13 @@
 @RunWith(JUnit4::class)
 class ParsedArgsTest {
 
+  private val root = createTempDir()
+
+  @After
+  fun tearDown() {
+    root.deleteRecursively()
+  }
+
   @Test
   fun `files to format are returned and unknown flags are reported`() {
     val out = ByteArrayOutputStream()
@@ -40,6 +50,16 @@
   }
 
   @Test
+  fun `files to format are returned and flags starting with @ are reported`() {
+    val out = ByteArrayOutputStream()
+
+    val (fileNames, _) = ParsedArgs.parseOptions(PrintStream(out), arrayOf("foo.kt", "@unknown"))
+
+    assertThat(fileNames).containsExactly("foo.kt")
+    assertThat(out.toString()).isEqualTo("Unexpected option: @unknown\n")
+  }
+
+  @Test
   fun `parseOptions uses default values when args are empty`() {
     val out = ByteArrayOutputStream()
 
@@ -107,4 +127,30 @@
 
     assertThat(parsed.setExitIfChanged).isTrue()
   }
+
+  @Test
+  fun `processArgs use the @file option with non existing file`() {
+    val out = ByteArrayOutputStream()
+
+    try {
+      ParsedArgs.processArgs(PrintStream(out), arrayOf("@non-existing-file"))
+      fail("expected an exception of type FileNotFoundException but nothing was thrown")
+    } catch (e: FileNotFoundException) {
+      assertThat(e.message).contains("non-existing-file (No such file or directory)")
+    }
+  }
+
+  @Test
+  fun `processArgs use the @file option with file containing arguments`() {
+    val out = ByteArrayOutputStream()
+    val file = root.resolve("existing-file")
+    file.writeText("--google-style\n--dry-run\n--set-exit-if-changed\nFile1.kt\nFile2.kt\n")
+
+    val parsed = ParsedArgs.processArgs(PrintStream(out), arrayOf("@" + file.absolutePath))
+
+    assertThat(parsed.formattingOptions).isEqualTo(Formatter.GOOGLE_FORMAT)
+    assertThat(parsed.dryRun).isTrue()
+    assertThat(parsed.setExitIfChanged).isTrue()
+    assertThat(parsed.fileNames).containsExactlyElementsIn(listOf("File1.kt", "File2.kt"))
+  }
 }