From 38acd801476fcc07056f47dc226d36e7ec299a52 Mon Sep 17 00:00:00 2001 From: eugene yokota Date: Sun, 1 Mar 2026 03:44:30 -0500 Subject: [PATCH] [2.x] fix: Fixes double quotes handling in fork mode (#8846) When using the arguments file (`@argsfile`) mechanism for forked runs, double quotes inside arguments were not escaped, causing the JVM's argument file parser to strip them. For example, passing `{"a":1}` as an argument would result in `{a:1}`. Escape `"` as `\"` in `createArgumentsFile`, matching the existing backslash escaping, so the JVM correctly round-trips quoted arguments. Fixes sbt/sbt#7129 Co-authored-by: BrianHotopp Co-authored-by: Claude Opus 4.6 --- run/src/main/scala/sbt/Fork.scala | 4 +-- run/src/test/scala/sbt/ForkTest.scala | 26 +++++++++++++++++++ .../run/fork-argsfile-quotes/build.sbt | 6 +++++ .../src/main/scala/Main.scala | 6 +++++ .../sbt-test/run/fork-argsfile-quotes/test | 2 ++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 sbt-app/src/sbt-test/run/fork-argsfile-quotes/build.sbt create mode 100644 sbt-app/src/sbt-test/run/fork-argsfile-quotes/src/main/scala/Main.scala create mode 100644 sbt-app/src/sbt-test/run/fork-argsfile-quotes/test diff --git a/run/src/main/scala/sbt/Fork.scala b/run/src/main/scala/sbt/Fork.scala index 2c4c102af..935f3fc91 100644 --- a/run/src/main/scala/sbt/Fork.scala +++ b/run/src/main/scala/sbt/Fork.scala @@ -166,7 +166,7 @@ object Fork { /** * Create an arguments file from a sequence of command line arguments - * by quoting each argument to a line with escaped backslashes + * by quoting each argument to a line with escaped backslashes and double quotes * * @param options command line options to write to the args file * @return @@ -178,7 +178,7 @@ object Fork { val pw = new PrintWriter(file) options.foreach { option => pw.write("\"") - pw.write(option.replace("\\", "\\\\")) + pw.write(option.replace("\\", "\\\\").replace("\"", "\\\"")) pw.write("\"") pw.write(System.lineSeparator()) } diff --git a/run/src/test/scala/sbt/ForkTest.scala b/run/src/test/scala/sbt/ForkTest.scala index 4faa668fa..68fc24aa4 100644 --- a/run/src/test/scala/sbt/ForkTest.scala +++ b/run/src/test/scala/sbt/ForkTest.scala @@ -64,6 +64,25 @@ object ForkTest extends Properties("Fork") { } } + property("Arguments with double quotes preserved in arguments file mode.") = { + val baos = new java.io.ByteArrayOutputStream() + val jsonArg = """{"a":1}""" + // Pad JVM options to exceed MaxConcatenatedOptionLength (5000) and trigger argsfile mode + val padding = "-Dproperty=" + ("X" * 5000) + val absClasspath = Path.makeString(requiredEntries) + val args = List("-cp", absClasspath, padding, "sbt.echoArgs", jsonArg) + val config = ForkOptions() + .withOutputStrategy(CustomOutput(baos)) + .withCanUseArgumentsFile(true) + val exitCode = + try Fork.java(config, args) + catch { case e: Exception => e.printStackTrace(); 1 } + val output = baos.toString("UTF-8").trim + s"exitCode: $exitCode" |: + s"output: '$output', expected: '$jsonArg'" |: + (exitCode == 0) && (output == jsonArg) + } + private def trimClasspath(cp: String): String = if (cp.length > MaximumClasspathLength) { val lastEntryI = cp.lastIndexOf(File.pathSeparatorChar.toInt, MaximumClasspathLength) @@ -80,3 +99,10 @@ object exit { System.exit(java.lang.Integer.parseInt(args(0))) } } + +// Echoes each argument on its own line, used to verify argument passing +object echoArgs { + def main(args: Array[String]): Unit = { + args.foreach(println) + } +} diff --git a/sbt-app/src/sbt-test/run/fork-argsfile-quotes/build.sbt b/sbt-app/src/sbt-test/run/fork-argsfile-quotes/build.sbt new file mode 100644 index 000000000..7c326238c --- /dev/null +++ b/sbt-app/src/sbt-test/run/fork-argsfile-quotes/build.sbt @@ -0,0 +1,6 @@ +scalaVersion := "3.6.4" +run / fork := true +// Force arguments file mode by exceeding MaxConcatenatedOptionLength (5000) +run / javaOptions += ("-Dsome.long.property=" + ("X" * 5000)) +// Pass JSON with double quotes as a system property — this goes through the argsfile +run / javaOptions += """-Djson={"a":1}""" diff --git a/sbt-app/src/sbt-test/run/fork-argsfile-quotes/src/main/scala/Main.scala b/sbt-app/src/sbt-test/run/fork-argsfile-quotes/src/main/scala/Main.scala new file mode 100644 index 000000000..2507ba7a8 --- /dev/null +++ b/sbt-app/src/sbt-test/run/fork-argsfile-quotes/src/main/scala/Main.scala @@ -0,0 +1,6 @@ +object Main { + def main(args: Array[String]): Unit = { + val json = System.getProperty("json") + assert(json == """{"a":1}""", s"""Expected '{"a":1}' but got '$json'""") + } +} diff --git a/sbt-app/src/sbt-test/run/fork-argsfile-quotes/test b/sbt-app/src/sbt-test/run/fork-argsfile-quotes/test new file mode 100644 index 000000000..97e62c271 --- /dev/null +++ b/sbt-app/src/sbt-test/run/fork-argsfile-quotes/test @@ -0,0 +1,2 @@ +# Verify that double quotes in JVM options survive the argsfile round-trip (sbt/sbt#7129) +> run