diff --git a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala index af54da662..d27e37bd2 100644 --- a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala +++ b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala @@ -194,4 +194,36 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil: s"Machine config should appear before project config. machineIndex=$machineIndex, projectIndex=$projectIndex" ) + // Test for issue #7289: Special characters in .jvmopts should not cause shell expansion + testOutput( + "sbt with special characters in .jvmopts (pipes, wildcards, ampersands)", + jvmoptsFileContents = + "-Dtest.pipes=host1|host2|host3\n-Dtest.wildcards=path/*/pattern\n-Dtest.ampersand=value&other", + windowsSupport = false, + )("-v"): (out: List[String]) => + // Verify that properties with special characters are handled correctly + // The pipe characters should be treated literally, not as shell operators + assert( + out.contains[String]("-Dtest.pipes=host1|host2|host3"), + "Property with pipes should be handled correctly" + ) + assert( + out.contains[String]("-Dtest.wildcards=path/*/pattern"), + "Property with wildcards should be handled correctly" + ) + assert( + out.contains[String]("-Dtest.ampersand=value&other"), + "Property with ampersands should be handled correctly" + ) + // Verify no shell errors occurred (no "command not found" messages or "unexpected" errors) + val errorMessages = out.filter(line => + line.contains("command not found") || + line.contains("was unexpected at this time") || + line.contains("syntax error") + ) + assert( + errorMessages.isEmpty, + s"Should not have shell expansion errors, but found: ${errorMessages.mkString(", ")}" + ) + end RunnerScriptTest diff --git a/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala b/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala index ce6a10c11..3077e506a 100644 --- a/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala +++ b/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala @@ -35,96 +35,116 @@ trait ShellScriptUtil extends BasicTestSuite { sbtOptsFileContents: String = "", javaToolOptions: String = "", distSbtoptsContents: String = "", - machineSbtoptsContents: String = "" + machineSbtoptsContents: String = "", + jvmoptsFileContents: String = "", + windowsSupport: Boolean = true, )(args: String*)(f: List[String] => Any) = - test(name) { - val workingDirectory = Files.createTempDirectory("sbt-launcher-package-test").toFile - retry(() => IO.copyDirectory(new File("launcher-package/citest"), workingDirectory)) + if !windowsSupport && isWindows then + test(name): + cancel("test not supported on Windows") + else + test(name) { + val workingDirectory = Files.createTempDirectory("sbt-launcher-package-test").toFile + retry(() => IO.copyDirectory(new File("launcher-package/citest"), workingDirectory)) - var sbtHome: Option[File] = None - var configHome: Option[File] = None - var tempSbtHome: Option[File] = None - var testSbtScript: File = sbtScript - try - val sbtOptsFile = new File(workingDirectory, ".sbtopts") - sbtOptsFile.createNewFile() - val writer = new PrintWriter(sbtOptsFile) - try { - writer.write(sbtOptsFileContents) - } finally { - writer.close() - } + var sbtHome: Option[File] = None + var configHome: Option[File] = None + var tempSbtHome: Option[File] = None + var testSbtScript: File = sbtScript + try + val sbtOptsFile = new File(workingDirectory, ".sbtopts") + sbtOptsFile.createNewFile() + val writer = new PrintWriter(sbtOptsFile) + try { + writer.write(sbtOptsFileContents) + } finally { + writer.close() + } - val envVars = scala.collection.mutable.Map[String, String]() + // Create .jvmopts file if contents provided + if (jvmoptsFileContents.nonEmpty) { + val jvmoptsFile = new File(workingDirectory, ".jvmopts") + jvmoptsFile.createNewFile() + val jvmoptsWriter = new PrintWriter(jvmoptsFile) + try { + jvmoptsWriter.write(jvmoptsFileContents) + } finally { + jvmoptsWriter.close() + } + } - // Set up dist sbtopts if provided - // Note: sbt script derives sbt_home from script location, not SBT_HOME env var - // Copy the sbt staging directory to a temp location to avoid modifying the staging directory - if (distSbtoptsContents.nonEmpty) { - val originalSbtHome = sbtScript.getParentFile.getParentFile - val tempSbtHomeDir = Files.createTempDirectory("sbt-home-test").toFile - tempSbtHome = Some(tempSbtHomeDir) - // Copy the entire sbt home directory structure - retry(() => IO.copyDirectory(originalSbtHome, tempSbtHomeDir)) - // Get the script from the copied directory - val binDir = new File(tempSbtHomeDir, "bin") - testSbtScript = new File(binDir, sbtScript.getName) - // Create dist sbtopts in the copied directory - val distSbtoptsDir = new File(tempSbtHomeDir, "conf") - distSbtoptsDir.mkdirs() - val distSbtoptsFile = new File(distSbtoptsDir, "sbtopts") - IO.write(distSbtoptsFile, distSbtoptsContents) - // Store reference for cleanup - sbtHome = Some(tempSbtHomeDir) - } + val envVars = scala.collection.mutable.Map[String, String]() - // Ensure no machine sbtopts exists when testing dist-only (unless explicitly provided) - // The script only loads dist if machine doesn't exist - if (distSbtoptsContents.nonEmpty && machineSbtoptsContents.isEmpty && configHome.isEmpty) { - // Set XDG_CONFIG_HOME to a temp directory without sbtopts to prevent default machine sbtopts from being found - val emptyConfigHome = Files.createTempDirectory("empty-config-home").toFile - envVars("XDG_CONFIG_HOME") = emptyConfigHome.getAbsolutePath - // Also unset SBT_ETC_FILE if it exists - sys.env.get("SBT_ETC_FILE").foreach(_ => envVars("SBT_ETC_FILE") = "") - // Store for cleanup - configHome = Some(emptyConfigHome) - } + // Set up dist sbtopts if provided + // Note: sbt script derives sbt_home from script location, not SBT_HOME env var + // Copy the sbt staging directory to a temp location to avoid modifying the staging directory + if (distSbtoptsContents.nonEmpty) { + val originalSbtHome = sbtScript.getParentFile.getParentFile + val tempSbtHomeDir = Files.createTempDirectory("sbt-home-test").toFile + tempSbtHome = Some(tempSbtHomeDir) + // Copy the entire sbt home directory structure + retry(() => IO.copyDirectory(originalSbtHome, tempSbtHomeDir)) + // Get the script from the copied directory + val binDir = new File(tempSbtHomeDir, "bin") + testSbtScript = new File(binDir, sbtScript.getName) + // Create dist sbtopts in the copied directory + val distSbtoptsDir = new File(tempSbtHomeDir, "conf") + distSbtoptsDir.mkdirs() + val distSbtoptsFile = new File(distSbtoptsDir, "sbtopts") + IO.write(distSbtoptsFile, distSbtoptsContents) + // Store reference for cleanup + sbtHome = Some(tempSbtHomeDir) + } - // Set up machine sbtopts if provided - if (machineSbtoptsContents.nonEmpty) { - val configHomeDir = Files.createTempDirectory("config-home").toFile - configHome = Some(configHomeDir) - val machineSbtoptsDir = new File(configHomeDir, "sbt") - machineSbtoptsDir.mkdirs() - val machineSbtoptsFile = new File(machineSbtoptsDir, "sbtopts") - IO.write(machineSbtoptsFile, machineSbtoptsContents) - envVars("XDG_CONFIG_HOME") = configHomeDir.getAbsolutePath - } + // Ensure no machine sbtopts exists when testing dist-only (unless explicitly provided) + // The script only loads dist if machine doesn't exist + if ( + distSbtoptsContents.nonEmpty && machineSbtoptsContents.isEmpty && configHome.isEmpty + ) { + // Set XDG_CONFIG_HOME to a temp directory without sbtopts to prevent default machine sbtopts from being found + val emptyConfigHome = Files.createTempDirectory("empty-config-home").toFile + envVars("XDG_CONFIG_HOME") = emptyConfigHome.getAbsolutePath + // Also unset SBT_ETC_FILE if it exists + sys.env.get("SBT_ETC_FILE").foreach(_ => envVars("SBT_ETC_FILE") = "") + // Store for cleanup + configHome = Some(emptyConfigHome) + } - val path = sys.env.getOrElse("PATH", sys.env("Path")) - envVars("JAVA_OPTS") = javaOpts - envVars("SBT_OPTS") = sbtOpts - envVars("JAVA_TOOL_OPTIONS") = javaToolOptions - if (isWindows) - envVars("JAVACMD") = new File(javaBinDir, "java").getAbsolutePath() - else - envVars("PATH") = javaBinDir + File.pathSeparator + path + // Set up machine sbtopts if provided + if (machineSbtoptsContents.nonEmpty) { + val configHomeDir = Files.createTempDirectory("config-home").toFile + configHome = Some(configHomeDir) + val machineSbtoptsDir = new File(configHomeDir, "sbt") + machineSbtoptsDir.mkdirs() + val machineSbtoptsFile = new File(machineSbtoptsDir, "sbtopts") + IO.write(machineSbtoptsFile, machineSbtoptsContents) + envVars("XDG_CONFIG_HOME") = configHomeDir.getAbsolutePath + } - val out = scala.sys.process - .Process( - Seq(testSbtScript.getAbsolutePath) ++ args, - workingDirectory, - envVars.toSeq* - ) - .!! - .linesIterator - .toList - f(out) - () - finally - IO.delete(workingDirectory) - // Clean up temporary sbt home directory if we created one - tempSbtHome.foreach(IO.delete) - configHome.foreach(IO.delete) - } + val path = sys.env.getOrElse("PATH", sys.env("Path")) + envVars("JAVA_OPTS") = javaOpts + envVars("SBT_OPTS") = sbtOpts + envVars("JAVA_TOOL_OPTIONS") = javaToolOptions + if (isWindows) + envVars("JAVACMD") = new File(javaBinDir, "java").getAbsolutePath() + else + envVars("PATH") = javaBinDir + File.pathSeparator + path + + val out = scala.sys.process + .Process( + Seq(testSbtScript.getAbsolutePath) ++ args, + workingDirectory, + envVars.toSeq* + ) + .!! + .linesIterator + .toList + f(out) + () + finally + IO.delete(workingDirectory) + // Clean up temporary sbt home directory if we created one + tempSbtHome.foreach(IO.delete) + configHome.foreach(IO.delete) + } } diff --git a/sbt b/sbt index bdea93f31..39aa569cc 100755 --- a/sbt +++ b/sbt @@ -770,8 +770,11 @@ process_args () { loadConfigFile() { # Make sure the last line is read even if it doesn't have a terminating \n + # Output lines literally without shell expansion to handle special characters safely cat "$1" | sed $'/^\#/d;s/\r$//' | while read -r line || [[ -n "$line" ]]; do - eval echo $line + # Use printf with properly quoted variable to prevent shell expansion + # This safely handles special characters like |, *, &, etc. + printf '%s\n' "$line" done }