[2.x] fix: handle -sbt-dir with spaces from .sbtopts (#8875)

**Problem**

When you pass -sbt-dir "/Users/a' dog" on the command line, the launcher correctly produces:

-Dsbt.global.base=/Users/a' dog

But when you put the same option into .sbtopts, the launcher previously split the line on spaces without respecting quotes, so the resulting -Dsbt.global.base was truncated (for example, -Dsbt.global.base=/Users/a'). This made .sbtopts behavior inconsistent with CLI behavior and broke setups where the global sbt directory path contains spaces and an embedded quote.
This commit is contained in:
PandaMan 2026-03-05 13:41:30 +08:00 committed by GitHub
parent ed96b4e1e0
commit 119aa4d1a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 3 deletions

View File

@ -252,6 +252,31 @@ abstract class RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUt
s"sbtopts options should appear before CLI memory settings. g1Index=$g1Index, xmxCliIndex=$xmxCliIndex"
)
// Test for issue #7197: -sbt-dir with spaces (and quotes) in .sbtopts
// windowsSupport = false: skip on Windows cmd, but runs on Git Bash (#8779)
testOutput(
"sbt -sbt-dir with space and quote in .sbtopts",
sbtOptsFileContents = """-sbt-dir "/Users/a' dog"""",
windowsSupport = false,
)("-d", "-v"): (out: List[String]) =>
val cmdLineStart = out.indexWhere(_.contains("Executing command line"))
assert(cmdLineStart >= 0, "Command line section not found")
val cmdLine = out.drop(cmdLineStart + 1).takeWhile(!_.trim.isEmpty)
val globalBaseArgs =
cmdLine.filter(_.contains("Dsbt.global.base"))
assert(
globalBaseArgs.nonEmpty,
s"-Dsbt.global.base should be present in command line. cmdLine=${cmdLine.mkString(", ")}"
)
assert(
globalBaseArgs.exists(arg =>
arg.contains("= /Users/a' dog") || arg.contains("=/Users/a' dog")
),
s"-Dsbt.global.base should contain full path with space and quote. args=${globalBaseArgs.mkString(", ")}"
)
// Test for issue #7333: JVM parameters with spaces in .sbtopts
testOutput(
"sbt with -J--add-modules ALL-DEFAULT in .sbtopts (args with spaces)",

62
sbt
View File

@ -738,6 +738,47 @@ map_args () {
declare -p commands
}
# Parse a line into words, respecting single and double quotes.
# This is used for .sbtopts so that options like:
# -sbt-dir "/Users/a' dog"
# are split into two arguments: -sbt-dir and /Users/a' dog
parseLineIntoWords() {
local line="$1"
local word=""
local i=0
local len=${#line}
local in_dq=0
local in_sq=0
while (( i < len )); do
local c="${line:$i:1}"
if (( in_dq )); then
if [[ "$c" == '"' ]]; then
in_dq=0
else
word+="$c"
fi
elif (( in_sq )); then
if [[ "$c" == "'" ]]; then
in_sq=0
else
word+="$c"
fi
else
case "$c" in
'"') in_dq=1 ;;
"'") in_sq=1 ;;
' '|$'\t')
[[ -n "$word" ]] && printf '%s\n' "$word"
word=""
;;
*) word+="$c" ;;
esac
fi
((i++))
done
[[ -n "$word" ]] && printf '%s\n' "$word"
}
process_args () {
while [[ $# -gt 0 ]]; do
case "$1" in
@ -794,6 +835,21 @@ loadConfigFile() {
done
}
# Append tokens from an sbtopts-style file into the global sbt_file_opts array.
# Each non-empty, non-comment line is split using parseLineIntoWords so that
# quoted values with spaces (and embedded quotes) are preserved as single arguments.
appendSbtoptsFromFile() {
local file="$1"
[[ ! -f "$file" ]] && return
while IFS= read -r line || [[ -n "$line" ]]; do
line=$(printf '%s' "$line" | sed $'/^\#/d;s/[[:space:]]\{1,\}#.*//;s/\r$//')
[[ -z "$line" ]] && continue
while IFS= read -r token; do
[[ -n "$token" ]] && sbt_file_opts+=("$token")
done < <(parseLineIntoWords "$line")
done < "$file"
}
loadPropFile() {
# trim key and value so as to be more forgiving with spaces around the '=':
k=$(trimString $k)
@ -893,14 +949,14 @@ fi
# Pull in the machine-wide settings configuration.
if [[ -f "$machine_sbt_opts_file" ]]; then
sbt_file_opts+=($(loadConfigFile "$machine_sbt_opts_file"))
appendSbtoptsFromFile "$machine_sbt_opts_file"
else
# Otherwise pull in the default settings configuration.
[[ -f "$dist_sbt_opts_file" ]] && sbt_file_opts+=($(loadConfigFile "$dist_sbt_opts_file"))
[[ -f "$dist_sbt_opts_file" ]] && appendSbtoptsFromFile "$dist_sbt_opts_file"
fi
# Pull in the project-level config file, if it exists (highest priority, overrides machine/dist).
[[ -f "$sbt_opts_file" ]] && sbt_file_opts+=($(loadConfigFile "$sbt_opts_file"))
[[ -f "$sbt_opts_file" ]] && appendSbtoptsFromFile "$sbt_opts_file"
# Prepend sbtopts so command line args appear last and win for duplicate properties.
if (( ${#sbt_file_opts[@]} > 0 )); then