mirror of https://github.com/sbt/sbt.git
[2.x] fix: Fixes the handling of special characters in dot files (#8558)
- Replace 'eval echo $line' with 'printf "%s\n" "$line"' in loadConfigFile() - Prevents shell expansion of special characters like |, *, &, etc. - Fixes issue where properties with pipes, wildcards, and ampersands caused 'command not found' or 'unexpected' errors - Add test to verify special characters are handled correctly on all platforms
This commit is contained in:
parent
215e9d6325
commit
2f348a09b9
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
sbt
5
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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue