Skip to content

Commit 697e675

Browse files
Merge pull request #14263 from philwalk/cli-add-expression-eval-12648
add eval (-e) expression evaluation to command line
2 parents 2654b56 + c4494f9 commit 697e675

File tree

6 files changed

+201
-68
lines changed

6 files changed

+201
-68
lines changed

compiler/src/dotty/tools/MainGenericRunner.scala

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,26 @@ package dotty.tools
44
import scala.annotation.tailrec
55
import scala.io.Source
66
import scala.util.{ Try, Success, Failure }
7-
import java.net.URLClassLoader
8-
import sys.process._
97
import java.io.File
108
import java.lang.Thread
119
import scala.annotation.internal.sharable
1210
import dotty.tools.dotc.util.ClasspathFromClassloader
1311
import dotty.tools.runner.ObjectRunner
1412
import dotty.tools.dotc.config.Properties.envOrNone
1513
import java.util.jar._
16-
import java.util.jar.Attributes.Name
1714
import dotty.tools.io.Jar
1815
import dotty.tools.runner.ScalaClassLoader
1916
import java.nio.file.{Files, Paths, Path}
20-
import scala.collection.JavaConverters._
2117
import dotty.tools.dotc.config.CommandLineParser
18+
import dotty.tools.scripting.StringDriver
2219

2320
enum ExecuteMode:
2421
case Guess
2522
case Script
2623
case Repl
2724
case Run
2825
case PossibleRun
26+
case Expression
2927

3028
case class Settings(
3129
verbose: Boolean = false,
@@ -38,6 +36,7 @@ case class Settings(
3836
possibleEntryPaths: List[String] = List.empty,
3937
scriptArgs: List[String] = List.empty,
4038
targetScript: String = "",
39+
targetExpression: String = "",
4140
targetToRun: String = "",
4241
save: Boolean = false,
4342
modeShouldBePossibleRun: Boolean = false,
@@ -78,6 +77,9 @@ case class Settings(
7877
def withTargetToRun(targetToRun: String): Settings =
7978
this.copy(targetToRun = targetToRun)
8079

80+
def withExpression(scalaSource: String): Settings =
81+
this.copy(targetExpression = scalaSource)
82+
8183
def withSave: Settings =
8284
this.copy(save = true)
8385

@@ -149,6 +151,13 @@ object MainGenericRunner {
149151
process(remainingArgs, settings)
150152
case (o @ colorOption(_*)) :: tail =>
151153
process(tail, settings.withScalaArgs(o))
154+
case "-e" :: expression :: tail =>
155+
val mainSource = s"@main def main(args: String *): Unit =\n ${expression}"
156+
settings
157+
.withExecuteMode(ExecuteMode.Expression)
158+
.withExpression(mainSource)
159+
.withScriptArgs(tail*)
160+
.noSave // -save not useful here
152161
case arg :: tail =>
153162
val line = Try(Source.fromFile(arg).getLines.toList).toOption.flatMap(_.headOption)
154163
lazy val hasScalaHashbang = { val s = line.getOrElse("") ; s.startsWith("#!") && s.contains("scala") }
@@ -161,6 +170,7 @@ object MainGenericRunner {
161170
val newSettings = if arg.startsWith("-") then settings else settings.withPossibleEntryPaths(arg).withModeShouldBePossibleRun
162171
process(tail, newSettings.withResidualArgs(arg))
163172

173+
164174
def main(args: Array[String]): Unit =
165175
val scalaOpts = envOrNone("SCALA_OPTS").toArray.flatMap(_.split(" ")).filter(_.nonEmpty)
166176
val allArgs = scalaOpts ++ args
@@ -235,6 +245,16 @@ object MainGenericRunner {
235245
++ List("-script", settings.targetScript)
236246
++ settings.scriptArgs
237247
scripting.Main.main(properArgs.toArray)
248+
case ExecuteMode.Expression =>
249+
val cp = settings.classPath match {
250+
case Nil => ""
251+
case list => list.mkString(classpathSeparator)
252+
}
253+
val cpArgs = if cp.isEmpty then Nil else List("-classpath", cp)
254+
val properArgs = cpArgs ++ settings.residualArgs ++ settings.scalaArgs
255+
val driver = StringDriver(properArgs.toArray, settings.targetExpression)
256+
driver.compileAndRun(settings.classPath)
257+
238258
case ExecuteMode.Guess =>
239259
if settings.modeShouldBePossibleRun then
240260
run(settings.withExecuteMode(ExecuteMode.PossibleRun))

compiler/src/dotty/tools/scripting/ScriptingDriver.scala

Lines changed: 5 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,12 @@ package dotty.tools.scripting
22

33
import java.nio.file.{ Files, Paths, Path }
44
import java.io.File
5-
import java.net.{ URL, URLClassLoader }
6-
import java.lang.reflect.{ Modifier, Method }
5+
import java.net.{ URLClassLoader }
76

8-
import scala.jdk.CollectionConverters._
9-
10-
import dotty.tools.dotc.{ Driver, Compiler }
11-
import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ContextBase, ctx }
12-
import dotty.tools.dotc.config.CompilerCommand
7+
import dotty.tools.dotc.Driver
8+
import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ctx }
139
import dotty.tools.io.{ PlainDirectory, Directory, ClassPath }
14-
import dotty.tools.dotc.reporting.Reporter
15-
import dotty.tools.dotc.config.Settings.Setting._
16-
17-
import sys.process._
10+
import Util.*
1811

1912
class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver:
2013
def compileAndRun(pack:(Path, Seq[Path], String) => Boolean = null): Unit =
@@ -31,7 +24,7 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
3124
try
3225
val classpath = s"${ctx.settings.classpath.value}${pathsep}${sys.props("java.class.path")}"
3326
val classpathEntries: Seq[Path] = ClassPath.expandPath(classpath, expandStar=true).map { Paths.get(_) }
34-
val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, classpathEntries, scriptFile)
27+
val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, classpathEntries, scriptFile.toString)
3528
val invokeMain: Boolean =
3629
Option(pack) match
3730
case Some(func) =>
@@ -48,58 +41,6 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
4841
case None =>
4942
end compileAndRun
5043

51-
private def deleteFile(target: File): Unit =
52-
if target.isDirectory then
53-
for member <- target.listFiles.toList
54-
do deleteFile(member)
55-
target.delete()
56-
end deleteFile
57-
58-
private def detectMainClassAndMethod(outDir: Path, classpathEntries: Seq[Path],
59-
scriptFile: File): (String, Method) =
60-
61-
val classpathUrls = (classpathEntries :+ outDir).map { _.toUri.toURL }
62-
63-
val cl = URLClassLoader(classpathUrls.toArray)
64-
65-
def collectMainMethods(target: File, path: String): List[(String, Method)] =
66-
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
67-
val targetPath =
68-
if path.nonEmpty then s"${path}.${nameWithoutExtension}"
69-
else nameWithoutExtension
70-
71-
if target.isDirectory then
72-
for
73-
packageMember <- target.listFiles.toList
74-
membersMainMethod <- collectMainMethods(packageMember, targetPath)
75-
yield membersMainMethod
76-
else if target.getName.endsWith(".class") then
77-
val cls = cl.loadClass(targetPath)
78-
try
79-
val method = cls.getMethod("main", classOf[Array[String]])
80-
if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil
81-
catch
82-
case _: java.lang.NoSuchMethodException => Nil
83-
else Nil
84-
end collectMainMethods
85-
86-
val candidates = for
87-
file <- outDir.toFile.listFiles.toList
88-
method <- collectMainMethods(file, "")
89-
yield method
90-
91-
candidates match
92-
case Nil =>
93-
throw ScriptingException(s"No main methods detected in script ${scriptFile}")
94-
case _ :: _ :: _ =>
95-
throw ScriptingException("A script must contain only one main method. " +
96-
s"Detected the following main methods:\n${candidates.mkString("\n")}")
97-
case m :: Nil => m
98-
end match
99-
end detectMainClassAndMethod
100-
101-
def pathsep = sys.props("path.separator")
102-
10344
end ScriptingDriver
10445

10546
case class ScriptingException(msg: String) extends RuntimeException(msg)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package dotty.tools.scripting
2+
3+
import java.nio.file.{ Files, Paths, Path }
4+
5+
import dotty.tools.dotc.Driver
6+
import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ctx }
7+
import dotty.tools.io.{ PlainDirectory, Directory, ClassPath }
8+
import Util.*
9+
10+
class StringDriver(compilerArgs: Array[String], scalaSource: String) extends Driver:
11+
override def sourcesRequired: Boolean = false
12+
13+
def compileAndRun(classpath: List[String] = Nil): Unit =
14+
val outDir = Files.createTempDirectory("scala3-expression")
15+
outDir.toFile.deleteOnExit()
16+
17+
setup(compilerArgs, initCtx.fresh) match
18+
case Some((toCompile, rootCtx)) =>
19+
given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir,
20+
new PlainDirectory(Directory(outDir)))
21+
22+
val compiler = newCompiler
23+
compiler.newRun.compileFromStrings(List(scalaSource))
24+
25+
val output = ctx.settings.outputDir.value
26+
if ctx.reporter.hasErrors then
27+
throw StringDriverException("Errors encountered during compilation")
28+
29+
try
30+
val classpath = s"${ctx.settings.classpath.value}${pathsep}${sys.props("java.class.path")}"
31+
val classpathEntries: Seq[Path] = ClassPath.expandPath(classpath, expandStar=true).map { Paths.get(_) }
32+
sys.props("java.class.path") = classpathEntries.map(_.toString).mkString(pathsep)
33+
val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, classpathEntries, scalaSource)
34+
mainMethod.invoke(null, Array.empty[String])
35+
catch
36+
case e: java.lang.reflect.InvocationTargetException =>
37+
throw e.getCause
38+
finally
39+
deleteFile(outDir.toFile)
40+
case None =>
41+
end compileAndRun
42+
43+
end StringDriver
44+
45+
case class StringDriverException(msg: String) extends RuntimeException(msg)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package dotty.tools.scripting
2+
3+
import java.nio.file.{ Path }
4+
import java.io.File
5+
import java.net.{ URLClassLoader }
6+
import java.lang.reflect.{ Modifier, Method }
7+
8+
object Util:
9+
10+
def deleteFile(target: File): Unit =
11+
if target.isDirectory then
12+
for member <- target.listFiles.toList
13+
do deleteFile(member)
14+
target.delete()
15+
end deleteFile
16+
17+
def detectMainClassAndMethod(outDir: Path, classpathEntries: Seq[Path], srcFile: String): (String, Method) =
18+
val classpathUrls = (classpathEntries :+ outDir).map { _.toUri.toURL }
19+
val cl = URLClassLoader(classpathUrls.toArray)
20+
21+
def collectMainMethods(target: File, path: String): List[(String, Method)] =
22+
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
23+
val targetPath =
24+
if path.nonEmpty then s"${path}.${nameWithoutExtension}"
25+
else nameWithoutExtension
26+
27+
if target.isDirectory then
28+
for
29+
packageMember <- target.listFiles.toList
30+
membersMainMethod <- collectMainMethods(packageMember, targetPath)
31+
yield membersMainMethod
32+
else if target.getName.endsWith(".class") then
33+
val cls = cl.loadClass(targetPath)
34+
try
35+
val method = cls.getMethod("main", classOf[Array[String]])
36+
if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil
37+
catch
38+
case _: java.lang.NoSuchMethodException => Nil
39+
else Nil
40+
end collectMainMethods
41+
42+
val mains = for
43+
file <- outDir.toFile.listFiles.toList
44+
method <- collectMainMethods(file, "")
45+
yield method
46+
47+
mains match
48+
case Nil =>
49+
throw StringDriverException(s"No main methods detected for [${srcFile}]")
50+
case _ :: _ :: _ =>
51+
throw StringDriverException(
52+
s"internal error: Detected the following main methods:\n${mains.mkString("\n")}")
53+
case m :: Nil => m
54+
end match
55+
end detectMainClassAndMethod
56+
57+
def pathsep = sys.props("path.separator")
58+
59+
end Util
60+

compiler/test/dotty/tools/scripting/BashScriptsTests.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dotty
22
package tools
33
package scripting
44

5+
import java.nio.file.Paths
56
import org.junit.{Test, AfterClass}
67
import org.junit.Assert.assertEquals
78

@@ -195,6 +196,8 @@ class BashScriptsTests:
195196
val scriptBase = "sqlDateError"
196197
val scriptFile = testFiles.find(_.getName == s"$scriptBase.sc").get
197198
val testJar = testFile(s"$scriptBase.jar") // jar should not be created when scriptFile runs
199+
val tj = Paths.get(testJar).toFile
200+
if tj.isFile then tj.delete() // discard residual debris from previous test
198201
printf("===> verify '-save' is cancelled by '-nosave' in script hashbang.`\n")
199202
val (validTest, exitCode, stdout, stderr) = bashCommand(s"SCALA_OPTS=-save ${scriptFile.absPath}")
200203
printf("stdout: %s\n", stdout.mkString("\n","\n",""))
@@ -209,3 +212,18 @@ class BashScriptsTests:
209212
assert(valid, s"script ${scriptFile.absPath} reported unexpected value for java.sql.Date ${stdout.mkString("\n")}")
210213
assert(!testJar.exists,s"unexpected, jar file [$testJar] was created")
211214

215+
216+
/*
217+
* verify -e println("yo!") works.
218+
*/
219+
@Test def verifyCommandLineExpression =
220+
printf("===> verify -e <expression> is properly handled by `dist/bin/scala`\n")
221+
val expected = "9"
222+
val expression = s"println(3*3)"
223+
val cmd = s"bin/scala -e $expression"
224+
val (validTest, exitCode, stdout, stderr) = bashCommand(s"""bin/scala -e '$expression'""")
225+
val result = stdout.filter(_.nonEmpty).mkString("")
226+
printf("stdout: %s\n", result)
227+
printf("stderr: %s\n", stderr.mkString("\n","\n",""))
228+
if verifyValid(validTest) then
229+
assert(result.contains(expected), s"expression [$expression] did not send [$expected] to stdout")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package dotty
2+
package tools
3+
package scripting
4+
5+
import java.nio.file.Paths
6+
import org.junit.{Test, AfterClass}
7+
import org.junit.Assert.assertEquals
8+
9+
import vulpix.TestConfiguration
10+
11+
import ScriptTestEnv.*
12+
13+
/**
14+
* +. test scala -e <expression>
15+
*/
16+
class ExpressionTest:
17+
/*
18+
* verify -e <expression> works.
19+
*/
20+
@Test def verifyCommandLineExpression =
21+
printf("===> verify -e <expression> is properly handled by `dist/bin/scala`\n")
22+
val expected = "9"
23+
val expression = s"println(3*3)"
24+
val result = getResult(expression)
25+
assert(result.contains(expected), s"expression [$expression] did not send [$expected] to stdout")
26+
27+
@Test def verifyImports: Unit =
28+
val expressionLines = List(
29+
"import java.nio.file.Paths",
30+
"""val cwd = Paths.get(""."")""",
31+
"""println(cwd.toFile.listFiles.toList.filter(_.isDirectory).size)""",
32+
)
33+
val expression = expressionLines.mkString(";")
34+
testExpression(expression){ result =>
35+
result.matches("[0-9]+") && result.toInt > 0
36+
}
37+
38+
def getResult(expression: String): String =
39+
val cmd = s"bin/scala -e $expression"
40+
val (_, _, stdout, stderr) = bashCommand(s"""bin/scala -e '$expression'""")
41+
printf("stdout: %s\n", stdout.mkString("|"))
42+
printf("stderr: %s\n", stderr.mkString("\n","\n",""))
43+
stdout.filter(_.nonEmpty).mkString("")
44+
45+
def testExpression(expression: String)(check: (result: String) => Boolean) = {
46+
val result = getResult(expression)
47+
check(result)
48+
}
49+

0 commit comments

Comments
 (0)