From da3b82d01d1c42c4820f09195e3937d50b1d521c Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Sat, 16 Nov 2024 14:16:44 +0100 Subject: [PATCH 01/11] add config option to run code on startup --- core/src/main/scala/replpp/Config.scala | 19 +++++++++++++++++-- core/src/test/scala/replpp/ConfigTests.scala | 3 +++ .../src/main/scala/replpp/server/Config.scala | 1 + .../scala/replpp/server/ConfigTests.scala | 2 ++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/replpp/Config.scala b/core/src/main/scala/replpp/Config.scala index 5395a8ca..fcb45ab9 100644 --- a/core/src/main/scala/replpp/Config.scala +++ b/core/src/main/scala/replpp/Config.scala @@ -8,7 +8,8 @@ import replpp.shaded.scopt.OParserBuilder import java.nio.file.Path case class Config( - predefFiles: Seq[Path] = Nil, + predefFiles: Seq[Path] = Nil, // these files will be precompiled and added to the classpath + runBefore: Seq[String] = Nil, // these statements will be interpreted on startup nocolors: Boolean = false, verbose: Boolean = false, classpathConfig: Config.ForClasspath = Config.ForClasspath(), @@ -41,6 +42,10 @@ case class Config( add("--predef", predefFile.toString) } + runBefore.foreach { runBefore => + add("--runBefore", runBefore) + } + if (nocolors) add("--nocolors") if (verbose) add("--verbose") @@ -103,6 +108,7 @@ object Config { OParser.sequence( programName("scala-repl-pp"), opts.predef((x, c) => c.copy(predefFiles = c.predefFiles :+ x)), + opts.runBefore((x, c) => c.copy(runBefore = c.runBefore :+ x)), opts.nocolors((_, c) => c.copy(nocolors = true)), opts.verbose((_, c) => c.copy(verbose = true)), opts.classpathEntry((x, c) => c.copy(classpathConfig = c.classpathConfig.copy(additionalClasspathEntries = c.classpathConfig.additionalClasspathEntries :+ x))), @@ -142,7 +148,16 @@ object Config { .unbounded() .optional() .action(action) - .text("import additional script files on startup - may be passed multiple times") + .text("given source files will be compiled and added to classpath - this may be passed multiple times") + } + + def runBefore[C](using builder: OParserBuilder[C])(action: Action[String, C]) = { + builder.opt[String]("runBefore") + .valueName("val foo = 42") + .unbounded() + .optional() + .action(action) + .text("given code will be executed on startup - this may be passed multiple times") } def nocolors[C](using builder: OParserBuilder[C])(action: Action[Unit, C]) = { diff --git a/core/src/test/scala/replpp/ConfigTests.scala b/core/src/test/scala/replpp/ConfigTests.scala index 8f32c74a..ee48586b 100644 --- a/core/src/test/scala/replpp/ConfigTests.scala +++ b/core/src/test/scala/replpp/ConfigTests.scala @@ -19,6 +19,7 @@ class ConfigTests extends AnyWordSpec with Matchers { "asJavaArgs (inverse of Config.parse)" in { val config = Config( predefFiles = List(Paths.get("/some/path/predefFile1"), Paths.get("/some/path/predefFile2")), + runBefore = List("val foo = 42", "println(foo)"), nocolors = true, verbose = true, classpathConfig = Config.ForClasspath( @@ -39,6 +40,8 @@ class ConfigTests extends AnyWordSpec with Matchers { javaArgs shouldBe Seq( "--predef", Paths.get("/some/path/predefFile1").toString, "--predef", Paths.get("/some/path/predefFile2").toString, + "--runBefore", "val foo = 42", + "--runBefore", "println(foo)", "--nocolors", "--verbose", "--classpathEntry", "cp1", diff --git a/server/src/main/scala/replpp/server/Config.scala b/server/src/main/scala/replpp/server/Config.scala index 48afa047..eaaaafec 100644 --- a/server/src/main/scala/replpp/server/Config.scala +++ b/server/src/main/scala/replpp/server/Config.scala @@ -16,6 +16,7 @@ object Config { val parser = OParser.sequence( programName("scala-repl-pp-server"), replpp.Config.opts.predef((x, c) => c.copy(baseConfig = c.baseConfig.copy(predefFiles = c.baseConfig.predefFiles :+ x))), + replpp.Config.opts.runBefore((x, c) => c.copy(baseConfig = c.baseConfig.copy(runBefore = c.baseConfig.runBefore :+ x))), replpp.Config.opts.verbose((_, c) => c.copy(baseConfig = c.baseConfig.copy(verbose = true))), replpp.Config.opts.inheritClasspath((_, c) => c.copy(baseConfig = c.baseConfig.copy(classpathConfig = c.baseConfig.classpathConfig.copy(inheritClasspath = true)))), replpp.Config.opts.classpathIncludesEntry((x, c) => { diff --git a/server/src/test/scala/replpp/server/ConfigTests.scala b/server/src/test/scala/replpp/server/ConfigTests.scala index 55924ab6..dea6d3a4 100644 --- a/server/src/test/scala/replpp/server/ConfigTests.scala +++ b/server/src/test/scala/replpp/server/ConfigTests.scala @@ -17,6 +17,7 @@ class ConfigTests extends AnyWordSpec with Matchers { "--server-auth-password", "test-pass", "--verbose", "--predef", "test-predef.sc", + "--runBefore", "val foo = 42", )) parsed.serverHost shouldBe "testHost" parsed.serverPort shouldBe 42 @@ -24,6 +25,7 @@ class ConfigTests extends AnyWordSpec with Matchers { parsed.serverAuthPassword shouldBe Some("test-pass") parsed.baseConfig.verbose shouldBe true parsed.baseConfig.predefFiles shouldBe Seq(Paths.get("test-predef.sc")) + parsed.baseConfig.runBefore shouldBe Seq("val foo = 42") } } From f0852d0f8e1145273e64fd3ace942d82ef567919 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 08:28:21 +0100 Subject: [PATCH 02/11] scripting driver: print warning for given `runBefore` code --- .../main/scala/replpp/scripting/NonForkingScriptRunner.scala | 5 +++++ core/src/main/scala/replpp/scripting/ScriptingDriver.scala | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala b/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala index 1fcb91c4..51dde94f 100644 --- a/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala +++ b/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala @@ -1,5 +1,6 @@ package replpp.scripting +import org.slf4j.LoggerFactory import replpp.{Config, allPredefFiles, allSourceFiles} import java.nio.file.Files @@ -14,6 +15,7 @@ import scala.util.{Failure, Success} * the NonForkingScriptRunner :) */ object NonForkingScriptRunner { + private val logger = LoggerFactory.getLogger(getClass) def main(args: Array[String]): Unit = { val config = Config.parse(args) @@ -36,6 +38,9 @@ object NonForkingScriptRunner { commandArgs ++ parameterArgs } + if (config.runBefore.nonEmpty) + logger.warn(s"ScriptingDriver does not support `runBefore` code, the given ${config.runBefore.size} statements will be ignored") + val verboseEnabled = replpp.verboseEnabled(config) new ScriptingDriver( compilerArgs = replpp.compilerArgs(config) :+ "-nowarn", diff --git a/core/src/main/scala/replpp/scripting/ScriptingDriver.scala b/core/src/main/scala/replpp/scripting/ScriptingDriver.scala index 4602a17c..299fdd21 100644 --- a/core/src/main/scala/replpp/scripting/ScriptingDriver.scala +++ b/core/src/main/scala/replpp/scripting/ScriptingDriver.scala @@ -17,7 +17,7 @@ import scala.util.{Failure, Try} * Runs a given script on the current JVM. * * Similar to dotty.tools.scripting.ScriptingDriver, but simpler and faster. - * Main difference: we don't (need to) recursively look for main method entrypoints in the entire classpath, + * Main difference: we don't (need to) recursively look for main method entry points in the entire classpath, * because we have a fixed class and method name that ScriptRunner uses when it embeds the script and predef code. * */ class ScriptingDriver(compilerArgs: Array[String], predefFiles: Seq[Path], scriptFile: Path, scriptArgs: Array[String], verbose: Boolean) { From 0d1d12fb322d79bd6d2c96bd97b485980b01fb9c Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 15:00:31 +0100 Subject: [PATCH 03/11] runBefore: invoke --- build.sbt | 2 +- core/src/main/scala/replpp/Config.scala | 2 +- .../src/main/scala/replpp/InteractiveShell.scala | 13 +++++++------ core/src/main/scala/replpp/package.scala | 16 +++++++--------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/build.sbt b/build.sbt index 788fa53a..93fde41f 100644 --- a/build.sbt +++ b/build.sbt @@ -26,7 +26,7 @@ lazy val core = project.in(file("core")) executableScriptName := "srp", libraryDependencies ++= Seq( "org.scala-lang" %% "scala3-compiler" % scalaVersion.value, - "org.slf4j" % "slf4j-simple" % "2.0.13" % Optional, + "org.slf4j" % "slf4j-simple" % "2.0.16" % Optional, ), assemblyJarName := "srp.jar", // TODO remove the '.jar' suffix - when doing so, it doesn't work any longer ) diff --git a/core/src/main/scala/replpp/Config.scala b/core/src/main/scala/replpp/Config.scala index fcb45ab9..5605b0f3 100644 --- a/core/src/main/scala/replpp/Config.scala +++ b/core/src/main/scala/replpp/Config.scala @@ -153,7 +153,7 @@ object Config { def runBefore[C](using builder: OParserBuilder[C])(action: Action[String, C]) = { builder.opt[String]("runBefore") - .valueName("val foo = 42") + .valueName("'val foo = 42'") .unbounded() .optional() .action(action) diff --git a/core/src/main/scala/replpp/InteractiveShell.scala b/core/src/main/scala/replpp/InteractiveShell.scala index b819ee61..68f040b7 100644 --- a/core/src/main/scala/replpp/InteractiveShell.scala +++ b/core/src/main/scala/replpp/InteractiveShell.scala @@ -2,6 +2,7 @@ package replpp import dotty.tools.repl.State +import java.lang.System.lineSeparator import scala.util.control.NoStackTrace object InteractiveShell { @@ -21,22 +22,22 @@ object InteractiveShell { ) val initialState: State = replDriver.initialState - val predefCode = DefaultPredef + val runBeforeLines = (DefaultRunBeforeLines ++ config.runBefore).mkString(lineSeparator) val state: State = { if (verboseEnabled(config)) { println(s"compiler arguments: ${compilerArgs.mkString(",")}") - println(predefCode) - replDriver.run(predefCode)(using initialState) + println(runBeforeLines) + replDriver.run(runBeforeLines)(using initialState) } else { - replDriver.runQuietly(predefCode)(using initialState) + replDriver.runQuietly(runBeforeLines)(using initialState) } } - if (predefCode.nonEmpty && state.objectIndex != 1) { + if (runBeforeLines.nonEmpty && state.objectIndex != 1) { throw new AssertionError(s"compilation error for predef code - error should have been reported above ^") with NoStackTrace } replDriver.runUntilQuit(using state)() } -} \ No newline at end of file +} diff --git a/core/src/main/scala/replpp/package.scala b/core/src/main/scala/replpp/package.scala index 8a1f91c9..f6ba9076 100644 --- a/core/src/main/scala/replpp/package.scala +++ b/core/src/main/scala/replpp/package.scala @@ -9,21 +9,20 @@ package object replpp { val VerboseEnvVar = "SCALA_REPL_PP_VERBOSE" lazy val pwd: Path = Paths.get(".").toAbsolutePath lazy val home: Path = Paths.get(System.getProperty("user.home")) - lazy val globalPredefFile = home.resolve(".scala-repl-pp.sc") - lazy val globalPredefFileMaybe = Option(globalPredefFile).filter(Files.exists(_)) + lazy val globalRunBeforeFile = home.resolve(".scala-repl-pp.sc") + lazy val globalRunBeforeFileMaybe = Option(globalRunBeforeFile).filter(Files.exists(_)) - private[replpp] def DefaultPredefLines(using colors: Colors) = { + private[replpp] def DefaultRunBeforeLines(using colors: Colors) = { val colorsImport = colors match { case Colors.BlackWhite => "replpp.Colors.BlackWhite" case Colors.Default => "replpp.Colors.Default" } Seq( "import replpp.Operators.*", - s"given replpp.Colors = $colorsImport" + s"given replpp.Colors = $colorsImport", ) } - private[replpp] def DefaultPredef(using Colors) = DefaultPredefLines.mkString(lineSeparator) /** verbose mode can either be enabled via the config, or the environment variable `SCALA_REPL_PP_VERBOSE=true` */ def verboseEnabled(config: Config): Boolean = { @@ -49,9 +48,8 @@ package object replpp { def allPredefFiles(config: Config): Seq[Path] = { val allPredefFiles = mutable.Set.empty[Path] allPredefFiles ++= config.predefFiles - globalPredefFileMaybe.foreach(allPredefFiles.addOne) - // the directly resolved predef files might reference additional files via `using` directive + // the directly referenced predef files might reference additional files via `using` directive val predefFilesDirect = allPredefFiles.toSet predefFilesDirect.foreach { predefFile => val importedFiles = UsingDirectives.findImportedFilesRecursively(predefFile, visited = allPredefFiles.toSet) @@ -68,11 +66,11 @@ package object replpp { } def allSourceLines(config: Config): Seq[String] = - allSourceFiles(config).flatMap(linesFromFile) + allSourceFiles(config).flatMap(linesFromFile) ++ config.runBefore /** precompile given predef files (if any) and update Config to include the results in the classpath */ def precompilePredefFiles(config: Config): Config = { - val allPredefFiles = (config.predefFiles :+ globalPredefFile).filter(Files.exists(_)) + val allPredefFiles = (config.predefFiles :+ globalRunBeforeFile).filter(Files.exists(_)) if (allPredefFiles.nonEmpty) { val predefClassfilesDir = new SimpleDriver().compileAndGetOutputDir( replpp.compilerArgs(config), From 9dd60c951d9c6362e0b258a6aca9b0ae0c348e2f Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 15:07:04 +0100 Subject: [PATCH 04/11] back to println for warning printing to avoid classloader issues --- .../scala/replpp/scripting/NonForkingScriptRunner.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala b/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala index 51dde94f..ade7690b 100644 --- a/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala +++ b/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala @@ -1,6 +1,5 @@ package replpp.scripting -import org.slf4j.LoggerFactory import replpp.{Config, allPredefFiles, allSourceFiles} import java.nio.file.Files @@ -15,7 +14,6 @@ import scala.util.{Failure, Success} * the NonForkingScriptRunner :) */ object NonForkingScriptRunner { - private val logger = LoggerFactory.getLogger(getClass) def main(args: Array[String]): Unit = { val config = Config.parse(args) @@ -39,7 +37,7 @@ object NonForkingScriptRunner { } if (config.runBefore.nonEmpty) - logger.warn(s"ScriptingDriver does not support `runBefore` code, the given ${config.runBefore.size} statements will be ignored") + println(s"[WARNING] ScriptingDriver does not support `runBefore` code, the given ${config.runBefore.size} statements will be ignored") val verboseEnabled = replpp.verboseEnabled(config) new ScriptingDriver( @@ -57,4 +55,4 @@ object NonForkingScriptRunner { } } -} \ No newline at end of file +} From 2267e919cd6e41082cd46237f0f5ee331d3a2fec Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 15:38:18 +0100 Subject: [PATCH 05/11] cleanup and update tests --- .../main/scala/replpp/UsingDirectives.scala | 16 ------------- core/src/test/scala/replpp/PredefTests.scala | 23 ++++++++----------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/core/src/main/scala/replpp/UsingDirectives.scala b/core/src/main/scala/replpp/UsingDirectives.scala index 03cb97b3..af5fa5c6 100644 --- a/core/src/main/scala/replpp/UsingDirectives.scala +++ b/core/src/main/scala/replpp/UsingDirectives.scala @@ -25,22 +25,6 @@ object UsingDirectives { def findImportedFiles(lines: IterableOnce[String], rootPath: Path): Seq[Path] = scanFor(FileDirective, lines).iterator.map(resolveFile(rootPath, _)).toSeq - def findImportedFilesRecursively(lines: IterableOnce[String], rootPath: Path): Seq[Path] = { - val results = Seq.newBuilder[Path] - val visited = mutable.Set.empty[Path] - - findImportedFiles(lines, rootPath).foreach { file => - results += file - visited += file - - val recursiveFiles = findImportedFilesRecursively(file, visited.toSet) - results ++= recursiveFiles - visited ++= recursiveFiles - } - - results.result().distinct - } - def findDeclaredDependencies(lines: IterableOnce[String]): Seq[String] = scanFor(LibDirective, lines) diff --git a/core/src/test/scala/replpp/PredefTests.scala b/core/src/test/scala/replpp/PredefTests.scala index cd907513..85419e6f 100644 --- a/core/src/test/scala/replpp/PredefTests.scala +++ b/core/src/test/scala/replpp/PredefTests.scala @@ -7,24 +7,19 @@ class PredefTests extends AnyWordSpec with Matchers { given Colors = Colors.BlackWhite "recursively resolve `//> using file` directive" in { - val script = os.temp("val mainScript = 5") - val additionalScript1 = os.temp("val additionalScript1 = 10") - val additionalScript2 = os.temp("val additionalScript2 = 20") + val additionalFile2 = os.temp( + """val predef2 = 10""" + ) + val additionalFile1 = os.temp( + s"""//> using file $additionalFile2 + |val predef1 = 20""".stripMargin) val predefFile = os.temp( - s"""//> using file $additionalScript1 + s"""//> using file $additionalFile1 |val predefCode = 1 - |//> using file $additionalScript2 |""".stripMargin) - allSourceLines(Config(predefFiles = Seq(predefFile.toNIO), scriptFile = Some(script.toNIO))).sorted shouldBe - Seq( - s"//> using file $additionalScript1", - s"//> using file $additionalScript2", - "val additionalScript1 = 10", - "val additionalScript2 = 20", - "val mainScript = 5", - "val predefCode = 1", - ).sorted + UsingDirectives.findImportedFilesRecursively(predefFile.toNIO).sorted shouldBe + Seq(additionalFile1, additionalFile2).map(_.toNIO).sorted } "recursively resolve `//> using file` directive - with recursive loops" in { From 99289a156488d8da94799e24c596d396648e8f1b Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 15:51:48 +0100 Subject: [PATCH 06/11] integrate / consolidate tests --- core/src/test/scala/replpp/PredefTests.scala | 54 ------------------ .../scala/replpp/UsingDirectivesTests.scala | 55 +++++++++++-------- 2 files changed, 33 insertions(+), 76 deletions(-) delete mode 100644 core/src/test/scala/replpp/PredefTests.scala diff --git a/core/src/test/scala/replpp/PredefTests.scala b/core/src/test/scala/replpp/PredefTests.scala deleted file mode 100644 index 85419e6f..00000000 --- a/core/src/test/scala/replpp/PredefTests.scala +++ /dev/null @@ -1,54 +0,0 @@ -package replpp - -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class PredefTests extends AnyWordSpec with Matchers { - given Colors = Colors.BlackWhite - - "recursively resolve `//> using file` directive" in { - val additionalFile2 = os.temp( - """val predef2 = 10""" - ) - val additionalFile1 = os.temp( - s"""//> using file $additionalFile2 - |val predef1 = 20""".stripMargin) - val predefFile = os.temp( - s"""//> using file $additionalFile1 - |val predefCode = 1 - |""".stripMargin) - - UsingDirectives.findImportedFilesRecursively(predefFile.toNIO).sorted shouldBe - Seq(additionalFile1, additionalFile2).map(_.toNIO).sorted - } - - "recursively resolve `//> using file` directive - with recursive loops" in { - val script = os.temp("val mainScript = 5") - val additionalScript1 = os.temp(suffix = "-script1") - val additionalScript2 = os.temp(suffix = "-script2") - - os.write.over(additionalScript1, - s"""//> using file $additionalScript2 - |val additionalScript1 = 10""".stripMargin) - os.write.over(additionalScript2, - s"""//> using file $additionalScript1 - |val additionalScript2 = 20 - |""".stripMargin) - - val predefFile = os.temp( - s"""//> using file $additionalScript1 - |val predefCode = 1 - |""".stripMargin) - - // most importantly, this should not loop endlessly due to the recursive imports - allSourceLines(Config(predefFiles = Seq(predefFile.toNIO), scriptFile = Some(script.toNIO))).distinct.sorted shouldBe - Seq( - s"//> using file $additionalScript1", - s"//> using file $additionalScript2", - "val additionalScript1 = 10", - "val additionalScript2 = 20", - "val mainScript = 5", - "val predefCode = 1", - ).sorted - } -} diff --git a/core/src/test/scala/replpp/UsingDirectivesTests.scala b/core/src/test/scala/replpp/UsingDirectivesTests.scala index c9dbfcf2..ed92a043 100644 --- a/core/src/test/scala/replpp/UsingDirectivesTests.scala +++ b/core/src/test/scala/replpp/UsingDirectivesTests.scala @@ -25,30 +25,41 @@ class UsingDirectivesTests extends AnyWordSpec with Matchers { results should not contain Paths.get("./commented_out.sc") } - "find imported files recursively from given source" in { - val script1 = os.temp("val foo = 42") - val script2 = os.temp( - s"""//> using file $script1 - |val bar = 42""".stripMargin) - - val rootPath = Paths.get(".") - val source = s"//> using file $script2" - - val results = UsingDirectives.findImportedFilesRecursively(Seq(source), rootPath) - results should contain(script1.toNIO) - results should contain(script2.toNIO) + "recursively resolve `//> using file` directive" in { + val additionalFile2 = os.temp( + contents = """val predef2 = 10""", + suffix = "additionalFile2" + ) + val additionalFile1 = os.temp( + contents = s"""//> using file $additionalFile2 + |val predef1 = 20""".stripMargin, + suffix = "additionalFile1" + ) + val predefFile = os.temp( + contents = s"""//> using file $additionalFile1 + |val predef0 = 0""".stripMargin) + + UsingDirectives.findImportedFilesRecursively(predefFile.toNIO).sorted shouldBe + Seq(additionalFile1, additionalFile2).map(_.toNIO).sorted } - "find imported files recursively from given script" in { - val script1 = os.temp("val foo = 42") - val script2 = os.temp( - s"""//> using file $script1 - |val bar = 42""".stripMargin) - val script3 = os.temp(s"//> using file $script2") - - val results = UsingDirectives.findImportedFilesRecursively(script3.toNIO) - results should contain(script1.toNIO) - results should contain(script2.toNIO) + "recursively resolve `//> using file` directive - and handle recursive loops" in { + val additionalFile2 = os.temp(suffix = "additionalFile2") + val additionalFile1 = os.temp(suffix = "additionalFile1") + val predefFile = os.temp( + contents = s"""//> using file $additionalFile1 + |val predef0 = 0""".stripMargin) + + os.write.over(additionalFile1, + s"""//> using file $additionalFile2 + |val predef1 = 10""".stripMargin) + os.write.over(additionalFile2, + s"""//> using file $additionalFile1 + |val predef2 = 20""".stripMargin) + + UsingDirectives.findImportedFilesRecursively(predefFile.toNIO).sorted shouldBe + Seq(additionalFile1, additionalFile2).map(_.toNIO).sorted + // most importantly, this should not loop endlessly due to the recursive imports } "find declared dependencies" in { From 360e247503a35c7eb3269747a284ac04a122b852 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 16:07:35 +0100 Subject: [PATCH 07/11] additional test --- core/src/main/scala/replpp/package.scala | 11 ++++----- .../replpp/scripting/ScriptRunnerTests.scala | 24 ++++++++++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/replpp/package.scala b/core/src/main/scala/replpp/package.scala index f6ba9076..b1412201 100644 --- a/core/src/main/scala/replpp/package.scala +++ b/core/src/main/scala/replpp/package.scala @@ -1,6 +1,5 @@ import replpp.util.{ClasspathHelper, SimpleDriver, linesFromFile} -import java.lang.System.lineSeparator import java.nio.file.{Files, Path, Paths} import scala.collection.mutable @@ -23,7 +22,6 @@ package object replpp { ) } - /** verbose mode can either be enabled via the config, or the environment variable `SCALA_REPL_PP_VERBOSE=true` */ def verboseEnabled(config: Config): Boolean = { config.verbose || @@ -43,7 +41,7 @@ package object replpp { /** recursively find all relevant source files from main script, global predef file, * provided predef files, other scripts that were imported with `using file` directive */ def allSourceFiles(config: Config): Seq[Path] = - (allPredefFiles(config) ++ config.scriptFile).distinct.sorted + (allPredefFiles(config) ++ config.scriptFile ++ globalRunBeforeFileMaybe).distinct.sorted def allPredefFiles(config: Config): Seq[Path] = { val allPredefFiles = mutable.Set.empty[Path] @@ -62,7 +60,7 @@ package object replpp { allPredefFiles ++= importedFiles } - allPredefFiles.toSeq.sorted + allPredefFiles.toSeq.filter(Files.exists(_)).sorted } def allSourceLines(config: Config): Seq[String] = @@ -70,11 +68,10 @@ package object replpp { /** precompile given predef files (if any) and update Config to include the results in the classpath */ def precompilePredefFiles(config: Config): Config = { - val allPredefFiles = (config.predefFiles :+ globalRunBeforeFile).filter(Files.exists(_)) - if (allPredefFiles.nonEmpty) { + if (config.predefFiles.nonEmpty) { val predefClassfilesDir = new SimpleDriver().compileAndGetOutputDir( replpp.compilerArgs(config), - inputFiles = allPredefFiles, + inputFiles = allPredefFiles(config), verbose = config.verbose ).get config.withAdditionalClasspathEntry(predefClassfilesDir) diff --git a/core/src/test/scala/replpp/scripting/ScriptRunnerTests.scala b/core/src/test/scala/replpp/scripting/ScriptRunnerTests.scala index 9f5a6651..0e56d7d8 100644 --- a/core/src/test/scala/replpp/scripting/ScriptRunnerTests.scala +++ b/core/src/test/scala/replpp/scripting/ScriptRunnerTests.scala @@ -129,7 +129,7 @@ class ScriptRunnerTests extends AnyWordSpec with Matchers { }.get shouldBe "iwashere-using-file-test3:99" } - "import additional files via `//> using file` recursively" in { + "import additional files via `//> using file` recursively from script file" in { execTest { testOutputPath => val additionalScript0 = os.temp() val additionalScript1 = os.temp() @@ -149,6 +149,28 @@ class ScriptRunnerTests extends AnyWordSpec with Matchers { }.get shouldBe "iwashere-using-file-test4:99" } + "import additional files via `//> using file` recursively from predef file" in { + execTest { testOutputPath => + val additionalPredefFile2 = os.temp() + val additionalPredefFile1 = os.temp() + // should handle recursive loop as well + os.write.over(additionalPredefFile2, + s"""//> using file $additionalPredefFile1 + |def foo = 99""".stripMargin) + os.write.over(additionalPredefFile1, + s"""//> using file $additionalPredefFile2 + |def bar = foo""".stripMargin) + val predefFile = os.temp(s"//> using file $additionalPredefFile1") + TestSetup( + s""" + |import java.nio.file.* + |Files.writeString(Path.of("$testOutputPath"), "iwashere-using-file-test4:" + bar) + |""".stripMargin, + _.copy(predefFiles = Seq(predefFile.toNIO)) + ) + }.get shouldBe "iwashere-using-file-test4:99" + } + "import additional files: use relative path of dependent script, and import in correct order" in { execTest { testOutputPath => val scriptRootDir = os.temp.dir() From 5da154f86d1837a7efd523debf15870f6fc3b32e Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 16:54:20 +0100 Subject: [PATCH 08/11] execute globalRunBeforeFile --- core/src/main/scala/replpp/InteractiveShell.scala | 2 +- .../main/scala/replpp/scripting/NonForkingScriptRunner.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/replpp/InteractiveShell.scala b/core/src/main/scala/replpp/InteractiveShell.scala index 68f040b7..e86d4fba 100644 --- a/core/src/main/scala/replpp/InteractiveShell.scala +++ b/core/src/main/scala/replpp/InteractiveShell.scala @@ -22,7 +22,7 @@ object InteractiveShell { ) val initialState: State = replDriver.initialState - val runBeforeLines = (DefaultRunBeforeLines ++ config.runBefore).mkString(lineSeparator) + val runBeforeLines = (DefaultRunBeforeLines ++ globalRunBeforeLines ++ config.runBefore).mkString(lineSeparator) val state: State = { if (verboseEnabled(config)) { println(s"compiler arguments: ${compilerArgs.mkString(",")}") diff --git a/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala b/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala index ade7690b..1d4e64e9 100644 --- a/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala +++ b/core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala @@ -1,6 +1,6 @@ package replpp.scripting -import replpp.{Config, allPredefFiles, allSourceFiles} +import replpp.{Config, allPredefFiles} import java.nio.file.Files import scala.util.{Failure, Success} From 60b24cbbcd864fb9063e9f1b7abbe7056a87f363 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 16:54:55 +0100 Subject: [PATCH 09/11] type annotations for readability --- core/src/main/scala/replpp/package.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/replpp/package.scala b/core/src/main/scala/replpp/package.scala index b1412201..332815d4 100644 --- a/core/src/main/scala/replpp/package.scala +++ b/core/src/main/scala/replpp/package.scala @@ -8,8 +8,9 @@ package object replpp { val VerboseEnvVar = "SCALA_REPL_PP_VERBOSE" lazy val pwd: Path = Paths.get(".").toAbsolutePath lazy val home: Path = Paths.get(System.getProperty("user.home")) - lazy val globalRunBeforeFile = home.resolve(".scala-repl-pp.sc") - lazy val globalRunBeforeFileMaybe = Option(globalRunBeforeFile).filter(Files.exists(_)) + lazy val globalRunBeforeFile: Path = home.resolve(".scala-repl-pp.sc") + lazy val globalRunBeforeFileMaybe: Option[Path] = Option(globalRunBeforeFile).filter(Files.exists(_)) + lazy val globalRunBeforeLines: Seq[String] = globalRunBeforeFileMaybe.map(linesFromFile).getOrElse(Seq.empty) private[replpp] def DefaultRunBeforeLines(using colors: Colors) = { val colorsImport = colors match { From 2cee8fb333ac51940ea3fc2bcf6fa1d8a47f3a78 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 17:01:06 +0100 Subject: [PATCH 10/11] document global runBefore file --- README.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c580fb27..3cb82fac 100644 --- a/README.md +++ b/README.md @@ -487,24 +487,16 @@ stringcalc> add(One, Two) val res0: stringcalc.Number = Number(3) ``` -## Global predef file: `~/.scala-repl-pp.sc` -Code that should be available across all srp sessions can be written into your local `~/.scala-repl-pp.sc`. +## Global runBefore file: `~/.scala-repl-pp.sc` +Code in this file will get executed at the start of each srp session, including scripts. ``` -echo 'def bar = 90' > ~/.scala-repl-pp.sc -echo 'def baz = 91' > script1.sc -echo 'def bam = 92' > script2.sc +echo 'import Short.MaxValue' > ~/.scala-repl-pp.sc -./srp --predef script1.sc --predef script2.sc - -scala> bar -val res0: Int = 90 - -scala> baz -val res1: Int = 91 +./srp -scala> bam -val res2: Int = 92 +scala> MaxValue +val res0: Int = 32767 ``` ## Verbose mode From 20caf8a00d45654a7979bb9702b4095410042e99 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Mon, 18 Nov 2024 18:31:00 +0100 Subject: [PATCH 11/11] readme --- README.md | 61 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3cb82fac..920e6386 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ srp Prerequisite: jdk11+ ## TOC - - [Benefits over / comparison with](#benefits-over--comparison-with) * [Regular Scala REPL](#regular-scala-repl) * [Ammonite](#ammonite) @@ -28,14 +27,15 @@ Prerequisite: jdk11+ - [Prerequisite for all of the below: run `sbt stage` or download the latest release](#prerequisite-for-all-of-the-below-run-sbt-stage-or-download-the-latest-release) - [REPL](#repl) * [run with defaults](#run-with-defaults) - * [customize prompt, greeting and exit code](#customize-prompt-greeting-and-exit-code) - * [execute some predef code](#execute-some-predef-code) + * [execute code at the start with `--runBefore`](#execute-code-at-the-start-with---runbefore) + * [`--predef`: code that is compiled but not executed](#--predef-code-that-is-compiled-but-not-executed) * [Operators: Redirect to file, pipe to external command](#operators-redirect-to-file-pipe-to-external-command) * [Add dependencies with maven coordinates](#add-dependencies-with-maven-coordinates) * [Importing additional script files interactively](#importing-additional-script-files-interactively) * [Adding classpath entries](#adding-classpath-entries) * [Rendering of output](#rendering-of-output) * [Exiting the REPL](#exiting-the-repl) + * [customize prompt, greeting and exit code](#customize-prompt-greeting-and-exit-code) * [Looking up the current terminal width](#looking-up-the-current-terminal-width) - [Scripting](#scripting) * [Simple "Hello world" script](#simple-hello-world-script) @@ -49,7 +49,6 @@ Prerequisite: jdk11+ * [Attach a debugger (remote jvm debug)](#attach-a-debugger-remote-jvm-debug) - [Server mode](#server-mode) - [Embed into your own project](#embed-into-your-own-project) -- [Global predef file: `~/.scala-repl-pp.sc`](#global-predef-file-scala-repl-ppsc) - [Verbose mode](#verbose-mode) - [Inherited classpath](#inherited-classpath) - [Parameters cheat sheet: the most important ones](#parameters-cheat-sheet-the-most-important-ones) @@ -62,7 +61,8 @@ Prerequisite: jdk11+ * [How can I get a new binary (bootstrapped) release?](#how-can-i-get-a-new-binary-bootstrapped-release) * [Updating the Scala version](#updating-the-scala-version) * [Updating the shaded libraries](#updating-the-shaded-libraries) -- [Fineprint](#fineprint) +- [Fineprint](#fineprint) + ## Benefits over / comparison with @@ -100,10 +100,28 @@ scala-cli wraps and invokes the regular Scala REPL (by default; or optionally Am ./srp ``` -### customize prompt, greeting and exit code -./srp --prompt myprompt --greeting 'hey there!' --onExitCode 'println("see ya!")' +### execute code at the start with `--runBefore` +``` +./srp --runBefore 'import Byte.MaxValue' + +scala> MaxValue +val res0: Int = 127 +``` + +You can specify this parameter multiple times, the given statements will be executed in the given order. + +If you want to execute some code _every single time_ you start a session, just write it to `~/.scala-repl-pp.sc` +``` +echo 'import Short.MaxValue' > ~/.scala-repl-pp.sc + +./srp + +scala> MaxValue +val res0: Int = 32767 +``` -### execute some predef code +### `--predef`: add source files to the classpath +Additional source files that are compiled added to the classpath, but unlike `runBefore` not executed straight away can be provided via `--predef`. ``` echo 'def foo = 42' > foo.sc @@ -112,6 +130,12 @@ scala> foo val res0: Int = 42 ``` +You can specify this parameter multiple times (`--predef one.sc --predef two.sc`). + +Why not use `runBefore` instead? For simple examples like the one above, you can do so. If it gets more complicated and you have multiple files referencing each others, `predef` allows you to treat it as one compilation unit, which isn't possible with `runBefore`. And as you add more code it's get's easier to manage in files rather than command line arguments. + +Note that predef files may not contain toplevel statements like `println("foo")` - instead, these either belong into your main script (if you're executing one) and/or can be passed to the repl via `runBefore`. + ### Operators: Redirect to file, pipe to external command Inspired by unix shell redirection and pipe operators (`>`, `>>` and `|`) you can redirect output into files with `#>` (overrides existing file) and `#>>` (create or append to file), and use `#|` to pipe the output to a command, such as `less`: ```scala @@ -274,6 +298,15 @@ $ ``` Context: we'd prefer to cancel the long-running operation, but that's not so easy on the JVM. +### customize prompt, greeting and exit code +``` +./srp --prompt myprompt --greeting 'hey there!' --onExitCode 'println("see ya!")' + +hey there! +myprompt> :exit +see ya! +``` + ### Looking up the current terminal width In case you want to adjust your output rendering to the available terminal size, you can look it up: @@ -487,18 +520,6 @@ stringcalc> add(One, Two) val res0: stringcalc.Number = Number(3) ``` -## Global runBefore file: `~/.scala-repl-pp.sc` -Code in this file will get executed at the start of each srp session, including scripts. - -``` -echo 'import Short.MaxValue' > ~/.scala-repl-pp.sc - -./srp - -scala> MaxValue -val res0: Int = 32767 -``` - ## Verbose mode If verbose mode is enabled, you'll get additional information about classpaths and complete scripts etc. To enable it, you can either pass `--verbose` or set the environment variable `SCALA_REPL_PP_VERBOSE=true`.