Skip to content

Commit 031e768

Browse files
MaciejG604Gedochao
andauthored
Accept --power from anywhere (#2399)
* Accept --power from whole command invocation * Dont consume --power from ScalaCli.scala, instead add --power to GlobalOptions * Add --power flag to SharedOptions --------- Co-authored-by: Piotr Chabelski <[email protected]>
1 parent 8010b19 commit 031e768

File tree

15 files changed

+244
-72
lines changed

15 files changed

+244
-72
lines changed

modules/cli/src/main/scala/scala/cli/ScalaCli.scala

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import scala.build.internal.Constants
1313
import scala.cli.config.{ConfigDb, Keys}
1414
import scala.cli.internal.Argv0
1515
import scala.cli.javaLauncher.JavaLauncherCli
16-
import scala.cli.launcher.{LauncherCli, LauncherOptions}
16+
import scala.cli.launcher.{LauncherCli, LauncherOptions, PowerOptions}
1717
import scala.cli.publish.BouncycastleSignerMaker
1818
import scala.cli.util.ConfigDbUtils
1919
import scala.util.Properties
@@ -221,7 +221,7 @@ object ScalaCli {
221221
launcherOpts.cliVersion.map(_.trim).filter(_.nonEmpty) match {
222222
case Some(ver) =>
223223
val powerArgs =
224-
if (launcherOpts.power) Seq("--power")
224+
if (launcherOpts.powerOptions.power) Seq("--power")
225225
else Nil
226226
val newArgs = powerArgs ++ args0
227227
LauncherCli.runAndExit(ver, launcherOpts, newArgs)
@@ -230,9 +230,21 @@ object ScalaCli {
230230
&& sys.props.get("scala-cli.kind").exists(_.startsWith("jvm")) =>
231231
JavaLauncherCli.runAndExit(args)
232232
case None =>
233-
if (launcherOpts.power)
233+
if launcherOpts.powerOptions.power then
234234
isSipScala = false
235-
args0.toArray
235+
args0.toArray
236+
else
237+
// Parse again to register --power at any position
238+
// Don't consume it, GlobalOptions parsing will do it
239+
PowerOptions.parser.ignoreUnrecognized.parse(args0) match {
240+
case Right((powerOptions, _)) =>
241+
if powerOptions.power then
242+
isSipScala = false
243+
args0.toArray
244+
case Left(e) =>
245+
System.err.println(e.message)
246+
sys.exit(1)
247+
}
236248
}
237249
}
238250
val (systemProps, scalaCliArgs) = partitionArgs(remainingArgs)

modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ abstract class ScalaCommand[T <: HasGlobalOptions](implicit myParser: Parser[T],
4141
override def hasFullHelp = true
4242
override def hidden = shouldExcludeInSip
4343
protected var argvOpt = Option.empty[Array[String]]
44+
4445
private val shouldExcludeInSip =
4546
(isRestricted || isExperimental) && !ScalaCli.allowRestrictedFeatures
4647
override def setArgv(argv: Array[String]): Unit = {

modules/cli/src/main/scala/scala/cli/commands/shared/GlobalOptions.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@ package scala.cli.commands.shared
22

33
import caseapp.*
44

5+
import scala.cli.launcher.PowerOptions
6+
57
case class GlobalOptions(
68
@Recurse
79
logging: LoggingOptions = LoggingOptions(),
810
@Recurse
9-
globalSuppress: GlobalSuppressWarningOptions = GlobalSuppressWarningOptions()
11+
globalSuppress: GlobalSuppressWarningOptions = GlobalSuppressWarningOptions(),
12+
13+
/** Duplication of [[scala.cli.launcher.LauncherOptions.powerOptions]]. Thanks to this, our unit
14+
* tests ensure that no subcommand defines an option that will clash with --power.
15+
*/
16+
@Recurse
17+
powerOptions: PowerOptions = PowerOptions()
1018
)
1119

1220
object GlobalOptions {

modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import scala.cli.commands.util.JvmUtils
4545
import scala.cli.commands.util.ScalacOptionsUtil.*
4646
import scala.cli.config.Key.BooleanEntry
4747
import scala.cli.config.{ConfigDb, Keys}
48+
import scala.cli.launcher.PowerOptions
4849
import scala.cli.util.ConfigDbUtils
4950
import scala.concurrent.ExecutionContextExecutorService
5051
import scala.concurrent.duration.*
@@ -61,6 +62,8 @@ final case class SharedOptions(
6162
suppress: SuppressWarningOptions = SuppressWarningOptions(),
6263
@Recurse
6364
logging: LoggingOptions = LoggingOptions(),
65+
@Recurse
66+
powerOptions: PowerOptions = PowerOptions(),
6467
@Recurse
6568
js: ScalaJsOptions = ScalaJsOptions(),
6669
@Recurse
@@ -223,7 +226,7 @@ final case class SharedOptions(
223226

224227
def logger: Logger = logging.logger
225228
override def global: GlobalOptions =
226-
GlobalOptions(logging = logging, globalSuppress = suppress.global)
229+
GlobalOptions(logging = logging, globalSuppress = suppress.global, powerOptions = powerOptions)
227230

228231
private def scalaJsOptions(opts: ScalaJsOptions): options.ScalaJsOptions = {
229232
import opts._

modules/cli/src/main/scala/scala/cli/launcher/LauncherOptions.scala

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ final case class LauncherOptions(
1919
@Hidden
2020
@Tag(tags.implementation)
2121
cliScalaVersion: Option[String] = None,
22-
@Group(HelpGroup.Launcher.toString)
23-
@HelpMessage("When called as 'scala', allow to use power commands too")
24-
@Tag(tags.must)
25-
power: Boolean = false
22+
@Recurse
23+
powerOptions: PowerOptions = PowerOptions()
2624
)
2725

2826
object LauncherOptions {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package scala.cli.launcher
2+
3+
import caseapp.*
4+
5+
import scala.cli.commands.shared.HelpGroup
6+
import scala.cli.commands.tags
7+
8+
/** Options extracted from [[LauncherOptions]] to allow for parsing them separately. Thanks to this
9+
* and additional parsing we can read the --power flag placed anywhere in the command invocation.
10+
*
11+
* This option is duplicated in [[scala.cli.commands.shared.GlobalOptions]] so that we can ensure
12+
* that no subcommand defines its own --power option Checking for clashing names is done in unit
13+
* tests.
14+
*/
15+
case class PowerOptions(
16+
@Group(HelpGroup.Launcher.toString)
17+
@HelpMessage("Allows to use restricted & experimental features")
18+
@Tag(tags.must)
19+
power: Boolean = false
20+
)
21+
22+
object PowerOptions {
23+
implicit val parser: Parser[PowerOptions] = Parser.derive
24+
implicit val help: Help[PowerOptions] = Help.derive
25+
}

modules/cli/src/test/scala/cli/tests/OptionsCheck.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scala.cli.tests
22

33
import scala.cli.ScalaCliCommands
4+
import scala.cli.commands.shared.HasGlobalOptions
45

56
class OptionsCheck extends munit.FunSuite {
67

@@ -12,4 +13,10 @@ class OptionsCheck extends munit.FunSuite {
1213
command.ensureNoDuplicates()
1314
}
1415

16+
test(s"--power option present in $command") {
17+
command.parser.stopAtFirstUnrecognized.parse(Seq("--power")) match {
18+
case Right((_: HasGlobalOptions, _ +: _)) => fail("Expected --power to be recognized")
19+
case _ => ()
20+
}
21+
}
1522
}

modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,30 @@ class ConfigTests extends ScalaCliSuite {
1616
val name = "Alex"
1717
TestInputs.empty.fromRoot { root =>
1818
val before =
19-
os.proc(TestUtil.cli, "--power", "config", "publish.user.name").call(
19+
// Test --power placed after subcommand name
20+
os.proc(TestUtil.cli, "config", "publish.user.name", "--power").call(
2021
cwd = root,
2122
env = configEnv
2223
)
2324
expect(before.out.trim().isEmpty)
2425

25-
os.proc(TestUtil.cli, "--power", "config", "publish.user.name", name).call(
26+
os.proc(TestUtil.cli, "config", "publish.user.name", name, "--power").call(
2627
cwd = root,
2728
env = configEnv
2829
)
2930
val res =
30-
os.proc(TestUtil.cli, "--power", "config", "publish.user.name").call(
31+
os.proc(TestUtil.cli, "config", "publish.user.name", "--power").call(
3132
cwd = root,
3233
env = configEnv
3334
)
3435
expect(res.out.trim() == name)
3536

36-
os.proc(TestUtil.cli, "--power", "config", "publish.user.name", "--unset").call(
37+
os.proc(TestUtil.cli, "config", "publish.user.name", "--unset", "--power").call(
3738
cwd = root,
3839
env = configEnv
3940
)
4041
val after =
41-
os.proc(TestUtil.cli, "--power", "config", "publish.user.name").call(
42+
os.proc(TestUtil.cli, "config", "publish.user.name", "--power").call(
4243
cwd = root,
4344
env = configEnv
4445
)

modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ abstract class ExportJsonTestDefinitions(val scalaVersionOpt: Option[String])
3838
TestUtil.initializeGit(root, "v1.1.2")
3939

4040
val exportJsonProc =
41-
os.proc(TestUtil.cli, "--power", "export", "--json", ".", "--jvm", "adopt:11")
41+
// Test --power placed after subcommand name
42+
os.proc(TestUtil.cli, "export", "--power", "--json", ".", "--jvm", "adopt:11")
4243
.call(cwd = root)
4344

4445
val jsonContents = readJson(exportJsonProc.out.text())

modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ abstract class PublishLocalTestDefinitions(val scalaVersionOpt: Option[String])
112112
def publishLocal(): os.CommandResult =
113113
os.proc(
114114
TestUtil.cli,
115-
"--power",
116115
"publish",
117116
"local",
118117
".",
119118
"--ivy2-home",
120119
os.rel / "ivy2",
120+
"--power", // Test --power placed after subcommand name
121121
"--working-dir",
122122
os.rel / "work-dir",
123123
extraOptions

0 commit comments

Comments
 (0)