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
1a50aa32f2
commit
02cd20e928
|
|
@ -194,4 +194,36 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil:
|
||||||
s"Machine config should appear before project config. machineIndex=$machineIndex, projectIndex=$projectIndex"
|
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
|
end RunnerScriptTest
|
||||||
|
|
|
||||||
|
|
@ -35,96 +35,116 @@ trait ShellScriptUtil extends BasicTestSuite {
|
||||||
sbtOptsFileContents: String = "",
|
sbtOptsFileContents: String = "",
|
||||||
javaToolOptions: String = "",
|
javaToolOptions: String = "",
|
||||||
distSbtoptsContents: String = "",
|
distSbtoptsContents: String = "",
|
||||||
machineSbtoptsContents: String = ""
|
machineSbtoptsContents: String = "",
|
||||||
|
jvmoptsFileContents: String = "",
|
||||||
|
windowsSupport: Boolean = true,
|
||||||
)(args: String*)(f: List[String] => Any) =
|
)(args: String*)(f: List[String] => Any) =
|
||||||
test(name) {
|
if !windowsSupport && isWindows then
|
||||||
val workingDirectory = Files.createTempDirectory("sbt-launcher-package-test").toFile
|
test(name):
|
||||||
retry(() => IO.copyDirectory(new File("launcher-package/citest"), workingDirectory))
|
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 sbtHome: Option[File] = None
|
||||||
var configHome: Option[File] = None
|
var configHome: Option[File] = None
|
||||||
var tempSbtHome: Option[File] = None
|
var tempSbtHome: Option[File] = None
|
||||||
var testSbtScript: File = sbtScript
|
var testSbtScript: File = sbtScript
|
||||||
try
|
try
|
||||||
val sbtOptsFile = new File(workingDirectory, ".sbtopts")
|
val sbtOptsFile = new File(workingDirectory, ".sbtopts")
|
||||||
sbtOptsFile.createNewFile()
|
sbtOptsFile.createNewFile()
|
||||||
val writer = new PrintWriter(sbtOptsFile)
|
val writer = new PrintWriter(sbtOptsFile)
|
||||||
try {
|
try {
|
||||||
writer.write(sbtOptsFileContents)
|
writer.write(sbtOptsFileContents)
|
||||||
} finally {
|
} finally {
|
||||||
writer.close()
|
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
|
val envVars = scala.collection.mutable.Map[String, String]()
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure no machine sbtopts exists when testing dist-only (unless explicitly provided)
|
// Set up dist sbtopts if provided
|
||||||
// The script only loads dist if machine doesn't exist
|
// Note: sbt script derives sbt_home from script location, not SBT_HOME env var
|
||||||
if (distSbtoptsContents.nonEmpty && machineSbtoptsContents.isEmpty && configHome.isEmpty) {
|
// Copy the sbt staging directory to a temp location to avoid modifying the staging directory
|
||||||
// Set XDG_CONFIG_HOME to a temp directory without sbtopts to prevent default machine sbtopts from being found
|
if (distSbtoptsContents.nonEmpty) {
|
||||||
val emptyConfigHome = Files.createTempDirectory("empty-config-home").toFile
|
val originalSbtHome = sbtScript.getParentFile.getParentFile
|
||||||
envVars("XDG_CONFIG_HOME") = emptyConfigHome.getAbsolutePath
|
val tempSbtHomeDir = Files.createTempDirectory("sbt-home-test").toFile
|
||||||
// Also unset SBT_ETC_FILE if it exists
|
tempSbtHome = Some(tempSbtHomeDir)
|
||||||
sys.env.get("SBT_ETC_FILE").foreach(_ => envVars("SBT_ETC_FILE") = "")
|
// Copy the entire sbt home directory structure
|
||||||
// Store for cleanup
|
retry(() => IO.copyDirectory(originalSbtHome, tempSbtHomeDir))
|
||||||
configHome = Some(emptyConfigHome)
|
// 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
|
// Ensure no machine sbtopts exists when testing dist-only (unless explicitly provided)
|
||||||
if (machineSbtoptsContents.nonEmpty) {
|
// The script only loads dist if machine doesn't exist
|
||||||
val configHomeDir = Files.createTempDirectory("config-home").toFile
|
if (
|
||||||
configHome = Some(configHomeDir)
|
distSbtoptsContents.nonEmpty && machineSbtoptsContents.isEmpty && configHome.isEmpty
|
||||||
val machineSbtoptsDir = new File(configHomeDir, "sbt")
|
) {
|
||||||
machineSbtoptsDir.mkdirs()
|
// Set XDG_CONFIG_HOME to a temp directory without sbtopts to prevent default machine sbtopts from being found
|
||||||
val machineSbtoptsFile = new File(machineSbtoptsDir, "sbtopts")
|
val emptyConfigHome = Files.createTempDirectory("empty-config-home").toFile
|
||||||
IO.write(machineSbtoptsFile, machineSbtoptsContents)
|
envVars("XDG_CONFIG_HOME") = emptyConfigHome.getAbsolutePath
|
||||||
envVars("XDG_CONFIG_HOME") = configHomeDir.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"))
|
// Set up machine sbtopts if provided
|
||||||
envVars("JAVA_OPTS") = javaOpts
|
if (machineSbtoptsContents.nonEmpty) {
|
||||||
envVars("SBT_OPTS") = sbtOpts
|
val configHomeDir = Files.createTempDirectory("config-home").toFile
|
||||||
envVars("JAVA_TOOL_OPTIONS") = javaToolOptions
|
configHome = Some(configHomeDir)
|
||||||
if (isWindows)
|
val machineSbtoptsDir = new File(configHomeDir, "sbt")
|
||||||
envVars("JAVACMD") = new File(javaBinDir, "java").getAbsolutePath()
|
machineSbtoptsDir.mkdirs()
|
||||||
else
|
val machineSbtoptsFile = new File(machineSbtoptsDir, "sbtopts")
|
||||||
envVars("PATH") = javaBinDir + File.pathSeparator + path
|
IO.write(machineSbtoptsFile, machineSbtoptsContents)
|
||||||
|
envVars("XDG_CONFIG_HOME") = configHomeDir.getAbsolutePath
|
||||||
|
}
|
||||||
|
|
||||||
val out = scala.sys.process
|
val path = sys.env.getOrElse("PATH", sys.env("Path"))
|
||||||
.Process(
|
envVars("JAVA_OPTS") = javaOpts
|
||||||
Seq(testSbtScript.getAbsolutePath) ++ args,
|
envVars("SBT_OPTS") = sbtOpts
|
||||||
workingDirectory,
|
envVars("JAVA_TOOL_OPTIONS") = javaToolOptions
|
||||||
envVars.toSeq*
|
if (isWindows)
|
||||||
)
|
envVars("JAVACMD") = new File(javaBinDir, "java").getAbsolutePath()
|
||||||
.!!
|
else
|
||||||
.linesIterator
|
envVars("PATH") = javaBinDir + File.pathSeparator + path
|
||||||
.toList
|
|
||||||
f(out)
|
val out = scala.sys.process
|
||||||
()
|
.Process(
|
||||||
finally
|
Seq(testSbtScript.getAbsolutePath) ++ args,
|
||||||
IO.delete(workingDirectory)
|
workingDirectory,
|
||||||
// Clean up temporary sbt home directory if we created one
|
envVars.toSeq*
|
||||||
tempSbtHome.foreach(IO.delete)
|
)
|
||||||
configHome.foreach(IO.delete)
|
.!!
|
||||||
}
|
.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
|
|
@ -767,8 +767,11 @@ process_args () {
|
||||||
|
|
||||||
loadConfigFile() {
|
loadConfigFile() {
|
||||||
# Make sure the last line is read even if it doesn't have a terminating \n
|
# 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
|
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
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue