[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 <brihoto@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
eugene yokota 2026-03-01 03:44:30 -05:00 committed by GitHub
parent f976330759
commit 38acd80147
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 42 additions and 2 deletions

View File

@ -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())
}

View File

@ -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)
}
}

View File

@ -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}"""

View File

@ -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'""")
}
}

View File

@ -0,0 +1,2 @@
# Verify that double quotes in JVM options survive the argsfile round-trip (sbt/sbt#7129)
> run