From 893e22102b29abb08926fb061c3abbcb7be9ba1f Mon Sep 17 00:00:00 2001 From: Anatolii Kmetiuk Date: Thu, 20 Aug 2026 02:25:00 +0900 Subject: [PATCH] [2.x] fix: Fixes -V parsing in sbt runners (#9626) Problem: -V is consumed by the launcher instead of sbt in sbt tasks -V. Solution: in the parser's -V behavior, lookback to see if there are commands going before it, in which case, forward -V to sbt. --- .../src/test/scala/ExtendedRunnerTest.scala | 6 ++ .../src/test/scala/RunnerScriptTest.scala | 61 +++++++++++++++++++ launcher-package/src/universal/bin/sbt.bat | 4 +- sbt | 15 ++++- sbtw/src/main/scala/sbtw/ArgParser.scala | 5 +- sbtw/src/test/scala/sbtw/ArgParserSpec.scala | 12 ++++ 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala b/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala index 87b9fc224..f001182aa 100755 --- a/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala +++ b/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala @@ -47,6 +47,12 @@ object ExtendedRunnerTest extends BasicTestSuite: () } + test("sbt tasks -V lists verbose tasks") { + val out = sbtProcessInDir(IntegrationTestPaths.citestDir("citest2"))("tasks", "-V").!! + assert(out.linesIterator.exists(_.contains("allCredentials"))) + assert(!out.linesIterator.exists(_.startsWith("sbt runner version:"))) + } + def testVersion(lines: List[String]): Unit = { assert(lines.size >= 2) val expected0 = s"(?m)^sbt version in this project: $versionRegEx(\\r)?" diff --git a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala index e2cd1fca5..d72bb87f0 100644 --- a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala +++ b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala @@ -154,6 +154,67 @@ abstract class RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUt assertVersionOutput(out) () + testOutput("sbt -V should work")("-V"): (out: List[String]) => + assertVersionOutput(out) + () + + testOutput("sbt -version should work")("-version"): (out: List[String]) => + assertVersionOutput(out) + () + + testOutput("sbt -V followed by a launcher option should work")("-V", "-v"): (out: List[String]) => + assertVersionOutput(out) + () + + testOutput("sbt launcher option followed by --version should work")("--no-colors", "--version"): + (out: List[String]) => + assertVersionOutput(out) + () + + testOutput("sbt --version followed by an empty argument should work")("--version", ""): + (out: List[String]) => + assertVersionOutput(out) + () + + testOutput("sbt tasks -V forwards the tasks option")("tasks", "-V", "-v"): (out: List[String]) => + assert(out.contains("-V")) + assert(!out.exists(_.startsWith("sbt runner version:"))) + + testOutput( + "sbt tasks -version forwards the tasks option", + windowsSupport = false, + )("tasks", "-version", "-v"): (out: List[String]) => + assert(out.contains("-version")) + assert(!out.exists(_.startsWith("sbt runner version:"))) + + testOutput( + "sbt tasks --version forwards the tasks option", + windowsSupport = false, + )("tasks", "--version", "-v"): (out: List[String]) => + assert(out.contains("--version")) + assert(!out.exists(_.startsWith("sbt runner version:"))) + + testOutput( + "sbt -V tasks forwards the tasks option", + windowsSupport = false, + )("-V", "tasks", "-v"): (out: List[String]) => + assert(out.contains("-V")) + assert(!out.exists(_.startsWith("sbt runner version:"))) + + testOutput( + "sbt -version tasks forwards the tasks option", + windowsSupport = false, + )("-version", "tasks", "-v"): (out: List[String]) => + assert(out.contains("-version")) + assert(!out.exists(_.startsWith("sbt runner version:"))) + + testOutput( + "sbt --version tasks forwards the tasks option", + windowsSupport = false, + )("--version", "tasks", "-v"): (out: List[String]) => + assert(out.contains("--version")) + assert(!out.exists(_.startsWith("sbt runner version:"))) + testOutput( "sbt --version reports spaced sbt.version from project/build.properties (sbt 1.x)", citestVariant = "citest", diff --git a/launcher-package/src/universal/bin/sbt.bat b/launcher-package/src/universal/bin/sbt.bat index b1ece361d..e6bbd353f 100755 --- a/launcher-package/src/universal/bin/sbt.bat +++ b/launcher-package/src/universal/bin/sbt.bat @@ -55,6 +55,7 @@ set sbt_args_client=-1 set sbt_args_jvm_client= set sbt_args_no_server= set sbt_args_experimental_execution_log= +set sbt_args_seen_command= set is_this_dir_sbt=0 rem users can set SBT_OPTS via .sbtopts @@ -184,7 +185,7 @@ if defined _verbose_arg ( goto args_loop ) -if "%~0" == "-V" set _version_arg=true +if "%~0" == "-V" if not defined sbt_args_seen_command set _version_arg=true if "%~0" == "-version" set _version_arg=true if "%~0" == "--version" set _version_arg=true @@ -607,6 +608,7 @@ if defined sbt_new if "%g:~0,2%" == "--" ( ) rem the %0 (instead of %~0) preserves original argument quoting +set sbt_args_seen_command=1 set SBT_ARGS=!SBT_ARGS! %0 goto args_loop diff --git a/sbt b/sbt index 87d2de303..196565ab7 100755 --- a/sbt +++ b/sbt @@ -801,7 +801,7 @@ process_args () { case "$1" in -h|-help|--help) print_help=1 && shift ;; -v|-verbose|--verbose) sbt_verbose=1 && shift ;; - -V|-version|--version) print_version=1 && shift ;; + -V|-version|--version) print_version=1 && addResidual "$1" && shift ;; --numeric-version) print_sbt_version=1 && shift ;; --script-version) print_sbt_script_version=1 && shift ;; shutdownall) shutdownall=1 && shift ;; @@ -841,6 +841,19 @@ process_args () { residual_args=() process_my_args "${myargs[@]}" } + + if [[ $print_version ]]; then + for arg in "${residual_args[@]}"; do + case "$arg" in + -V|-version|--version) ;; + *) if [[ "$arg" =~ [^[:space:]] ]]; then + print_version= + break + fi ;; + esac + done + [[ $print_version ]] && residual_args=() + fi } loadConfigFile() { diff --git a/sbtw/src/main/scala/sbtw/ArgParser.scala b/sbtw/src/main/scala/sbtw/ArgParser.scala index 8af66646d..d541b4eca 100644 --- a/sbtw/src/main/scala/sbtw/ArgParser.scala +++ b/sbtw/src/main/scala/sbtw/ArgParser.scala @@ -14,7 +14,10 @@ object ArgParser: opt[Unit]('h', "help").action((_, c) => c.copy(help = true)), opt[Unit]('v', "verbose").action((_, c) => c.copy(verbose = true)), opt[Unit]('d', "debug").action((_, c) => c.copy(debug = true)), - opt[Unit]('V', "version").action((_, c) => c.copy(version = true)), + opt[Unit]('V', "version").action((_, c) => + if c.residual.nonEmpty then c.copy(residual = c.residual :+ "-V") + else c.copy(version = true) + ), opt[Unit]("numeric-version").action((_, c) => c.copy(numericVersion = true)), opt[Unit]("script-version").action((_, c) => c.copy(scriptVersion = true)), opt[Unit]("shutdownall").action((_, c) => c.copy(shutdownAll = true)), diff --git a/sbtw/src/test/scala/sbtw/ArgParserSpec.scala b/sbtw/src/test/scala/sbtw/ArgParserSpec.scala index f807d1532..f8f3ca0d4 100644 --- a/sbtw/src/test/scala/sbtw/ArgParserSpec.scala +++ b/sbtw/src/test/scala/sbtw/ArgParserSpec.scala @@ -19,4 +19,16 @@ object ArgParserSpec extends verify.BasicTestSuite: val opts = ArgParser.parse(Array("--java-home", "C:\\jdk", "new")).get assert(opts.sbtNew) } + + test("standalone -V requests the runner version") { + val options = ArgParser.parse(Array("-V")).get + assert(options.version) + assert(options.residual.isEmpty) + } + + test("-V after a command is forwarded to sbt") { + val options = ArgParser.parse(Array("tasks", "-V")).get + assert(!options.version) + assert(options.residual == Seq("tasks", "-V")) + } end ArgParserSpec