From 119aa4d1a729104ee50aaf065719efa5e7b1c09e Mon Sep 17 00:00:00 2001 From: PandaMan Date: Thu, 5 Mar 2026 13:41:30 +0800 Subject: [PATCH] [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. --- .../src/test/scala/RunnerScriptTest.scala | 25 ++++++++ sbt | 62 ++++++++++++++++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala index 54484275d..722828e40 100644 --- a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala +++ b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala @@ -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)", diff --git a/sbt b/sbt index bb0bc606d..c0636009a 100755 --- a/sbt +++ b/sbt @@ -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