diff --git a/compiler/src/dotty/tools/MainGenericRunner.scala b/compiler/src/dotty/tools/MainGenericRunner.scala index 65ee672f0342..b7ef90dcdc60 100644 --- a/compiler/src/dotty/tools/MainGenericRunner.scala +++ b/compiler/src/dotty/tools/MainGenericRunner.scala @@ -4,8 +4,6 @@ package dotty.tools import scala.annotation.tailrec import scala.io.Source import scala.util.{ Try, Success, Failure } -import java.net.URLClassLoader -import sys.process._ import java.io.File import java.lang.Thread import scala.annotation.internal.sharable @@ -13,12 +11,11 @@ import dotty.tools.dotc.util.ClasspathFromClassloader import dotty.tools.runner.ObjectRunner import dotty.tools.dotc.config.Properties.envOrNone import java.util.jar._ -import java.util.jar.Attributes.Name import dotty.tools.io.Jar import dotty.tools.runner.ScalaClassLoader import java.nio.file.{Files, Paths, Path} -import scala.collection.JavaConverters._ import dotty.tools.dotc.config.CommandLineParser +import dotty.tools.scripting.StringDriver enum ExecuteMode: case Guess @@ -26,6 +23,7 @@ enum ExecuteMode: case Repl case Run case PossibleRun + case Expression case class Settings( verbose: Boolean = false, @@ -38,6 +36,7 @@ case class Settings( possibleEntryPaths: List[String] = List.empty, scriptArgs: List[String] = List.empty, targetScript: String = "", + targetExpression: String = "", targetToRun: String = "", save: Boolean = false, modeShouldBePossibleRun: Boolean = false, @@ -78,6 +77,9 @@ case class Settings( def withTargetToRun(targetToRun: String): Settings = this.copy(targetToRun = targetToRun) + def withExpression(scalaSource: String): Settings = + this.copy(targetExpression = scalaSource) + def withSave: Settings = this.copy(save = true) @@ -149,6 +151,13 @@ object MainGenericRunner { process(remainingArgs, settings) case (o @ colorOption(_*)) :: tail => process(tail, settings.withScalaArgs(o)) + case "-e" :: expression :: tail => + val mainSource = s"@main def main(args: String *): Unit =\n ${expression}" + settings + .withExecuteMode(ExecuteMode.Expression) + .withExpression(mainSource) + .withScriptArgs(tail*) + .noSave // -save not useful here case arg :: tail => val line = Try(Source.fromFile(arg).getLines.toList).toOption.flatMap(_.headOption) lazy val hasScalaHashbang = { val s = line.getOrElse("") ; s.startsWith("#!") && s.contains("scala") } @@ -161,6 +170,7 @@ object MainGenericRunner { val newSettings = if arg.startsWith("-") then settings else settings.withPossibleEntryPaths(arg).withModeShouldBePossibleRun process(tail, newSettings.withResidualArgs(arg)) + def main(args: Array[String]): Unit = val scalaOpts = envOrNone("SCALA_OPTS").toArray.flatMap(_.split(" ")).filter(_.nonEmpty) val allArgs = scalaOpts ++ args @@ -235,6 +245,16 @@ object MainGenericRunner { ++ List("-script", settings.targetScript) ++ settings.scriptArgs scripting.Main.main(properArgs.toArray) + case ExecuteMode.Expression => + val cp = settings.classPath match { + case Nil => "" + case list => list.mkString(classpathSeparator) + } + val cpArgs = if cp.isEmpty then Nil else List("-classpath", cp) + val properArgs = cpArgs ++ settings.residualArgs ++ settings.scalaArgs + val driver = StringDriver(properArgs.toArray, settings.targetExpression) + driver.compileAndRun(settings.classPath) + case ExecuteMode.Guess => if settings.modeShouldBePossibleRun then run(settings.withExecuteMode(ExecuteMode.PossibleRun)) diff --git a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala index cf3ebe671e1d..bce54183807b 100755 --- a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala +++ b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala @@ -2,19 +2,12 @@ package dotty.tools.scripting import java.nio.file.{ Files, Paths, Path } import java.io.File -import java.net.{ URL, URLClassLoader } -import java.lang.reflect.{ Modifier, Method } +import java.net.{ URLClassLoader } -import scala.jdk.CollectionConverters._ - -import dotty.tools.dotc.{ Driver, Compiler } -import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ContextBase, ctx } -import dotty.tools.dotc.config.CompilerCommand +import dotty.tools.dotc.Driver +import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ctx } import dotty.tools.io.{ PlainDirectory, Directory, ClassPath } -import dotty.tools.dotc.reporting.Reporter -import dotty.tools.dotc.config.Settings.Setting._ - -import sys.process._ +import Util.* class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver: def compileAndRun(pack:(Path, Seq[Path], String) => Boolean = null): Unit = @@ -31,7 +24,7 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: try val classpath = s"${ctx.settings.classpath.value}${pathsep}${sys.props("java.class.path")}" val classpathEntries: Seq[Path] = ClassPath.expandPath(classpath, expandStar=true).map { Paths.get(_) } - val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, classpathEntries, scriptFile) + val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, classpathEntries, scriptFile.toString) val invokeMain: Boolean = Option(pack) match case Some(func) => @@ -48,58 +41,6 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: case None => end compileAndRun - private def deleteFile(target: File): Unit = - if target.isDirectory then - for member <- target.listFiles.toList - do deleteFile(member) - target.delete() - end deleteFile - - private def detectMainClassAndMethod(outDir: Path, classpathEntries: Seq[Path], - scriptFile: File): (String, Method) = - - val classpathUrls = (classpathEntries :+ outDir).map { _.toUri.toURL } - - val cl = URLClassLoader(classpathUrls.toArray) - - def collectMainMethods(target: File, path: String): List[(String, Method)] = - val nameWithoutExtension = target.getName.takeWhile(_ != '.') - val targetPath = - if path.nonEmpty then s"${path}.${nameWithoutExtension}" - else nameWithoutExtension - - if target.isDirectory then - for - packageMember <- target.listFiles.toList - membersMainMethod <- collectMainMethods(packageMember, targetPath) - yield membersMainMethod - else if target.getName.endsWith(".class") then - val cls = cl.loadClass(targetPath) - try - val method = cls.getMethod("main", classOf[Array[String]]) - if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil - catch - case _: java.lang.NoSuchMethodException => Nil - else Nil - end collectMainMethods - - val candidates = for - file <- outDir.toFile.listFiles.toList - method <- collectMainMethods(file, "") - yield method - - candidates match - case Nil => - throw ScriptingException(s"No main methods detected in script ${scriptFile}") - case _ :: _ :: _ => - throw ScriptingException("A script must contain only one main method. " + - s"Detected the following main methods:\n${candidates.mkString("\n")}") - case m :: Nil => m - end match - end detectMainClassAndMethod - - def pathsep = sys.props("path.separator") - end ScriptingDriver case class ScriptingException(msg: String) extends RuntimeException(msg) diff --git a/compiler/src/dotty/tools/scripting/StringDriver.scala b/compiler/src/dotty/tools/scripting/StringDriver.scala new file mode 100755 index 000000000000..6ac0bce9766a --- /dev/null +++ b/compiler/src/dotty/tools/scripting/StringDriver.scala @@ -0,0 +1,45 @@ +package dotty.tools.scripting + +import java.nio.file.{ Files, Paths, Path } + +import dotty.tools.dotc.Driver +import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ctx } +import dotty.tools.io.{ PlainDirectory, Directory, ClassPath } +import Util.* + +class StringDriver(compilerArgs: Array[String], scalaSource: String) extends Driver: + override def sourcesRequired: Boolean = false + + def compileAndRun(classpath: List[String] = Nil): Unit = + val outDir = Files.createTempDirectory("scala3-expression") + outDir.toFile.deleteOnExit() + + setup(compilerArgs, initCtx.fresh) match + case Some((toCompile, rootCtx)) => + given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir, + new PlainDirectory(Directory(outDir))) + + val compiler = newCompiler + compiler.newRun.compileFromStrings(List(scalaSource)) + + val output = ctx.settings.outputDir.value + if ctx.reporter.hasErrors then + throw StringDriverException("Errors encountered during compilation") + + try + val classpath = s"${ctx.settings.classpath.value}${pathsep}${sys.props("java.class.path")}" + val classpathEntries: Seq[Path] = ClassPath.expandPath(classpath, expandStar=true).map { Paths.get(_) } + sys.props("java.class.path") = classpathEntries.map(_.toString).mkString(pathsep) + val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, classpathEntries, scalaSource) + mainMethod.invoke(null, Array.empty[String]) + catch + case e: java.lang.reflect.InvocationTargetException => + throw e.getCause + finally + deleteFile(outDir.toFile) + case None => + end compileAndRun + +end StringDriver + +case class StringDriverException(msg: String) extends RuntimeException(msg) diff --git a/compiler/src/dotty/tools/scripting/Util.scala b/compiler/src/dotty/tools/scripting/Util.scala new file mode 100755 index 000000000000..9529eb9ad791 --- /dev/null +++ b/compiler/src/dotty/tools/scripting/Util.scala @@ -0,0 +1,60 @@ +package dotty.tools.scripting + +import java.nio.file.{ Path } +import java.io.File +import java.net.{ URLClassLoader } +import java.lang.reflect.{ Modifier, Method } + +object Util: + + def deleteFile(target: File): Unit = + if target.isDirectory then + for member <- target.listFiles.toList + do deleteFile(member) + target.delete() + end deleteFile + + def detectMainClassAndMethod(outDir: Path, classpathEntries: Seq[Path], srcFile: String): (String, Method) = + val classpathUrls = (classpathEntries :+ outDir).map { _.toUri.toURL } + val cl = URLClassLoader(classpathUrls.toArray) + + def collectMainMethods(target: File, path: String): List[(String, Method)] = + val nameWithoutExtension = target.getName.takeWhile(_ != '.') + val targetPath = + if path.nonEmpty then s"${path}.${nameWithoutExtension}" + else nameWithoutExtension + + if target.isDirectory then + for + packageMember <- target.listFiles.toList + membersMainMethod <- collectMainMethods(packageMember, targetPath) + yield membersMainMethod + else if target.getName.endsWith(".class") then + val cls = cl.loadClass(targetPath) + try + val method = cls.getMethod("main", classOf[Array[String]]) + if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil + catch + case _: java.lang.NoSuchMethodException => Nil + else Nil + end collectMainMethods + + val mains = for + file <- outDir.toFile.listFiles.toList + method <- collectMainMethods(file, "") + yield method + + mains match + case Nil => + throw StringDriverException(s"No main methods detected for [${srcFile}]") + case _ :: _ :: _ => + throw StringDriverException( + s"internal error: Detected the following main methods:\n${mains.mkString("\n")}") + case m :: Nil => m + end match + end detectMainClassAndMethod + + def pathsep = sys.props("path.separator") + +end Util + diff --git a/compiler/test/dotty/tools/scripting/BashScriptsTests.scala b/compiler/test/dotty/tools/scripting/BashScriptsTests.scala index d6181da95ee4..b83b16e01e1f 100644 --- a/compiler/test/dotty/tools/scripting/BashScriptsTests.scala +++ b/compiler/test/dotty/tools/scripting/BashScriptsTests.scala @@ -2,6 +2,7 @@ package dotty package tools package scripting +import java.nio.file.Paths import org.junit.{Test, AfterClass} import org.junit.Assert.assertEquals @@ -195,6 +196,8 @@ class BashScriptsTests: val scriptBase = "sqlDateError" val scriptFile = testFiles.find(_.getName == s"$scriptBase.sc").get val testJar = testFile(s"$scriptBase.jar") // jar should not be created when scriptFile runs + val tj = Paths.get(testJar).toFile + if tj.isFile then tj.delete() // discard residual debris from previous test printf("===> verify '-save' is cancelled by '-nosave' in script hashbang.`\n") val (validTest, exitCode, stdout, stderr) = bashCommand(s"SCALA_OPTS=-save ${scriptFile.absPath}") printf("stdout: %s\n", stdout.mkString("\n","\n","")) @@ -209,3 +212,18 @@ class BashScriptsTests: assert(valid, s"script ${scriptFile.absPath} reported unexpected value for java.sql.Date ${stdout.mkString("\n")}") assert(!testJar.exists,s"unexpected, jar file [$testJar] was created") + + /* + * verify -e println("yo!") works. + */ + @Test def verifyCommandLineExpression = + printf("===> verify -e is properly handled by `dist/bin/scala`\n") + val expected = "9" + val expression = s"println(3*3)" + val cmd = s"bin/scala -e $expression" + val (validTest, exitCode, stdout, stderr) = bashCommand(s"""bin/scala -e '$expression'""") + val result = stdout.filter(_.nonEmpty).mkString("") + printf("stdout: %s\n", result) + printf("stderr: %s\n", stderr.mkString("\n","\n","")) + if verifyValid(validTest) then + assert(result.contains(expected), s"expression [$expression] did not send [$expected] to stdout") diff --git a/compiler/test/dotty/tools/scripting/ExpressionTest.scala b/compiler/test/dotty/tools/scripting/ExpressionTest.scala new file mode 100755 index 000000000000..fd642a7744e0 --- /dev/null +++ b/compiler/test/dotty/tools/scripting/ExpressionTest.scala @@ -0,0 +1,49 @@ +package dotty +package tools +package scripting + +import java.nio.file.Paths +import org.junit.{Test, AfterClass} +import org.junit.Assert.assertEquals + +import vulpix.TestConfiguration + +import ScriptTestEnv.* + +/** + * +. test scala -e + */ +class ExpressionTest: + /* + * verify -e works. + */ + @Test def verifyCommandLineExpression = + printf("===> verify -e is properly handled by `dist/bin/scala`\n") + val expected = "9" + val expression = s"println(3*3)" + val result = getResult(expression) + assert(result.contains(expected), s"expression [$expression] did not send [$expected] to stdout") + + @Test def verifyImports: Unit = + val expressionLines = List( + "import java.nio.file.Paths", + """val cwd = Paths.get(""."")""", + """println(cwd.toFile.listFiles.toList.filter(_.isDirectory).size)""", + ) + val expression = expressionLines.mkString(";") + testExpression(expression){ result => + result.matches("[0-9]+") && result.toInt > 0 + } + + def getResult(expression: String): String = + val cmd = s"bin/scala -e $expression" + val (_, _, stdout, stderr) = bashCommand(s"""bin/scala -e '$expression'""") + printf("stdout: %s\n", stdout.mkString("|")) + printf("stderr: %s\n", stderr.mkString("\n","\n","")) + stdout.filter(_.nonEmpty).mkString("") + + def testExpression(expression: String)(check: (result: String) => Boolean) = { + val result = getResult(expression) + check(result) + } +