Skip to content

Commit 7305250

Browse files
authored
Add support for execution of Scala, Java and script snippets (#1166)
1 parent 0a01cb6 commit 7305250

File tree

11 files changed

+447
-10
lines changed

11 files changed

+447
-10
lines changed

modules/build/src/main/scala/scala/build/Build.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ object Build {
7575
.replace(".", "_")
7676
.replace("/", ".")
7777
}
78-
case Left(stdin @ "stdin") => Some(s"${stdin}_sc")
79-
case _ => None
78+
case Left(virtual) if virtual == "stdin" || virtual == "snippet" =>
79+
Some(s"${virtual}_sc")
80+
case _ => None
8081
}
8182
val filteredMainClasses =
8283
mainClasses.filter(!scriptInferredMainClasses.contains(_))

modules/build/src/main/scala/scala/build/Inputs.scala

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ object Inputs {
186186
}
187187

188188
sealed abstract class VirtualSourceFile extends Virtual {
189-
def isStdin: Boolean = source.contains("<stdin>")
189+
def isStdin: Boolean = source.contains("<stdin>")
190+
def isSnippet: Boolean = source.contains("<snippet>")
190191
}
191192

192193
sealed trait SingleFile extends OnDisk with SingleElement
@@ -325,6 +326,32 @@ object Inputs {
325326
}
326327
}
327328

329+
def validateSnippets(
330+
scriptSnippetOpt: Option[String] = None,
331+
scalaSnippetOpt: Option[String] = None,
332+
javaSnippetOpt: Option[String] = None
333+
): Seq[Either[String, Seq[Element]]] = {
334+
def validateSnippet(
335+
maybeExpression: Option[String],
336+
f: Array[Byte] => Element
337+
): Option[Either[String, Seq[Element]]] =
338+
maybeExpression.filter(_.nonEmpty).map(expression =>
339+
Right(Seq(f(expression.getBytes(StandardCharsets.UTF_8))))
340+
)
341+
342+
Seq(
343+
validateSnippet(
344+
scriptSnippetOpt,
345+
content => VirtualScript(content, "snippet", os.sub / "snippet.sc")
346+
),
347+
validateSnippet(
348+
scalaSnippetOpt,
349+
content => VirtualScalaFile(content, "<snippet>-scala-file")
350+
),
351+
validateSnippet(javaSnippetOpt, content => VirtualJavaFile(content, "<snippet>-java-file"))
352+
).flatten
353+
}
354+
328355
def validateArgs(
329356
args: Seq[String],
330357
cwd: os.Path,
@@ -382,16 +409,22 @@ object Inputs {
382409
baseProjectName: String,
383410
download: String => Either[String, Array[Byte]],
384411
stdinOpt: => Option[Array[Byte]],
412+
scriptSnippetOpt: Option[String],
413+
scalaSnippetOpt: Option[String],
414+
javaSnippetOpt: Option[String],
385415
acceptFds: Boolean,
386416
forcedWorkspace: Option[os.Path]
387417
): Either[String, Inputs] = {
388418
val validatedArgs: Seq[Either[String, Seq[Element]]] =
389419
validateArgs(args, cwd, download, stdinOpt, acceptFds)
390-
val invalid = validatedArgs.collect {
420+
val validatedExpressions: Seq[Either[String, Seq[Element]]] =
421+
validateSnippets(scriptSnippetOpt, scalaSnippetOpt, javaSnippetOpt)
422+
val validatedArgsAndExprs = validatedArgs ++ validatedExpressions
423+
val invalid = validatedArgsAndExprs.collect {
391424
case Left(msg) => msg
392425
}
393426
if (invalid.isEmpty) {
394-
val validElems = validatedArgs.collect {
427+
val validElems = validatedArgsAndExprs.collect {
395428
case Right(elem) => elem
396429
}.flatten
397430
assert(validElems.nonEmpty)
@@ -410,10 +443,15 @@ object Inputs {
410443
defaultInputs: () => Option[Inputs] = () => None,
411444
download: String => Either[String, Array[Byte]] = _ => Left("URL not supported"),
412445
stdinOpt: => Option[Array[Byte]] = None,
446+
scriptSnippetOpt: Option[String] = None,
447+
scalaSnippetOpt: Option[String] = None,
448+
javaSnippetOpt: Option[String] = None,
413449
acceptFds: Boolean = false,
414450
forcedWorkspace: Option[os.Path] = None
415451
): Either[String, Inputs] =
416-
if (args.isEmpty)
452+
if (
453+
args.isEmpty && scriptSnippetOpt.isEmpty && scalaSnippetOpt.isEmpty && javaSnippetOpt.isEmpty
454+
)
417455
defaultInputs().toRight(
418456
"No inputs provided (expected files with .scala or .sc extensions, and / or directories)."
419457
)
@@ -425,6 +463,9 @@ object Inputs {
425463
baseProjectName,
426464
download,
427465
stdinOpt,
466+
scriptSnippetOpt,
467+
scalaSnippetOpt,
468+
javaSnippetOpt,
428469
acceptFds,
429470
forcedWorkspace
430471
)

modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ final case class JavaPreprocessor(
6464
case v: Inputs.VirtualJavaFile =>
6565
val res = either {
6666
val relPath =
67-
if (v.isStdin) {
67+
if (v.isStdin || v.isSnippet) {
6868
val classNameOpt = value {
6969
(new JavaParserProxyMaker)
7070
.get(
@@ -76,7 +76,7 @@ final case class JavaPreprocessor(
7676
}
7777
val fileName = classNameOpt
7878
.map(_ + ".java")
79-
.getOrElse("stdin.java")
79+
.getOrElse(if (v.isStdin) "stdin.java" else "java-snippet.java")
8080
os.sub / fileName
8181
}
8282
else v.subPath

modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ case object ScalaPreprocessor extends Preprocessor {
112112

113113
case v: Inputs.VirtualScalaFile =>
114114
val res = either {
115-
val relPath = if (v.isStdin) os.sub / "stdin.scala" else v.subPath
115+
val relPath = v match {
116+
case v if v.isStdin => os.sub / "stdin.scala"
117+
case v if v.isSnippet => os.sub / "scala-snippet.scala"
118+
case v => v.subPath
119+
}
116120
val content = new String(v.content, StandardCharsets.UTF_8)
117121
val (requirements, scopedRequirements, options, updatedContentOpt) =
118122
value(

modules/cli-options/src/main/scala/scala/cli/commands/SharedOptions.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ final case class SharedOptions(
4545

4646
@Group("Scala")
4747
@HelpMessage("Show help for scalac. This is an alias for --scalac-option -help")
48-
scalacHelp: Boolean = false,
48+
scalacHelp: Boolean = false,
49+
50+
@Recurse
51+
snippet: SnippetOptions = SnippetOptions(),
4952

5053
@Group("Java")
5154
@HelpMessage("Add extra JARs in the class path")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package scala.cli.commands
2+
3+
import caseapp._
4+
5+
// format: off
6+
final case class SnippetOptions(
7+
@Group("Scala")
8+
@HelpMessage("Allows to execute a passed string as a Scala script")
9+
@Name("e")
10+
@Name("executeScript")
11+
@Name("executeScalaScript")
12+
@Name("executeSc")
13+
scriptSnippet: Option[String] = None,
14+
15+
@Group("Scala")
16+
@HelpMessage("Allows to execute a passed string as Scala code")
17+
@Name("executeScala")
18+
scalaSnippet: Option[String] = None,
19+
20+
@Group("Java")
21+
@HelpMessage("Allows to execute a passed string as Java code")
22+
@Name("executeJava")
23+
javaSnippet: Option[String] = None,
24+
)
25+
// format: on
26+
27+
object SnippetOptions {
28+
implicit lazy val parser: Parser[SnippetOptions] = Parser.derive
29+
implicit lazy val help: Help[SnippetOptions] = Help.derive
30+
// Parser.Aux for using ExpressionOptions with @Recurse in other options
31+
implicit lazy val parserAux: Parser.Aux[SnippetOptions, parser.D] = parser
32+
33+
}

modules/cli/src/main/scala/scala/cli/commands/util/SharedOptionsUtil.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@ object SharedOptionsUtil {
268268
defaultInputs = defaultInputs,
269269
download = downloadInputs,
270270
stdinOpt = readStdin(logger = logger),
271+
scriptSnippetOpt = v.snippet.scriptSnippet,
272+
scalaSnippetOpt = v.snippet.scalaSnippet,
273+
javaSnippetOpt = v.snippet.javaSnippet,
271274
acceptFds = !Properties.isWin,
272275
forcedWorkspace = workspace.forcedWorkspaceOpt
273276
)

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,48 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
846846
expect(output == expectedOutput)
847847
}
848848
}
849+
test(
850+
"snippets mixed with piped Scala code and existing sources allow for cross-references"
851+
) {
852+
val hello = "Hello"
853+
val comma = ", "
854+
val world = "World"
855+
val exclamation = "!"
856+
val expectedOutput = hello + comma + world + exclamation
857+
val scriptSnippet = s"def world = \"$world\""
858+
val scalaSnippet = "case class ScalaSnippetData(value: String)"
859+
val javaSnippet =
860+
s"public class JavaSnippet { public static String exclamation = \"$exclamation\"; }"
861+
val pipedInput = s"def hello = \"$hello\""
862+
val inputs =
863+
TestInputs(Seq(os.rel / "Main.scala" ->
864+
s"""object Main extends App {
865+
| val hello = stdin.hello
866+
| val comma = ScalaSnippetData(value = "$comma").value
867+
| val world = snippet.world
868+
| val exclamation = JavaSnippet.exclamation
869+
| println(hello + comma + world + exclamation)
870+
|}
871+
|""".stripMargin))
872+
inputs.fromRoot { root =>
873+
val output =
874+
os.proc(
875+
TestUtil.cli,
876+
".",
877+
"_.sc",
878+
"--script-snippet",
879+
scriptSnippet,
880+
"--scala-snippet",
881+
scalaSnippet,
882+
"--java-snippet",
883+
javaSnippet,
884+
extraOptions
885+
)
886+
.call(cwd = root, stdin = pipedInput)
887+
.out.text().trim
888+
expect(output == expectedOutput)
889+
}
890+
}
849891
test("pick .scala main class over in-context scripts, including piped ones") {
850892
val inputs = TestInputs(
851893
Seq(
@@ -1959,4 +2001,44 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
19592001
expect(mainClasses == Set(scalaFile1, scalaFile2, s"$scriptsDir.${scriptName}_sc"))
19602002
}
19612003
}
2004+
2005+
test("correctly run a script snippet") {
2006+
emptyInputs.fromRoot { root =>
2007+
val msg =
2008+
"123456" // FIXME: change this to a a non-numeric string when Windows encoding is handled properly
2009+
val res = os.proc(TestUtil.cli, "-e", s"println($msg)", extraOptions).call(cwd = root)
2010+
expect(res.out.text().trim == msg)
2011+
}
2012+
}
2013+
2014+
test("correctly run a scala snippet") {
2015+
emptyInputs.fromRoot { root =>
2016+
val msg =
2017+
"123456" // FIXME: change this to a a non-numeric string when Windows encoding is handled properly
2018+
val res =
2019+
os.proc(
2020+
TestUtil.cli,
2021+
"--scala-snippet",
2022+
s"object Hello extends App { println($msg) }",
2023+
extraOptions
2024+
)
2025+
.call(cwd = root)
2026+
expect(res.out.text().trim == msg)
2027+
}
2028+
}
2029+
2030+
test("correctly run a java snippet") {
2031+
emptyInputs.fromRoot { root =>
2032+
val msg =
2033+
"123456" // FIXME: change this to a a non-numeric string when Windows encoding is handled properly
2034+
val res = os.proc(
2035+
TestUtil.cli,
2036+
"--java-snippet",
2037+
s"public class Main { public static void main(String[] args) { System.out.println($msg); } }",
2038+
extraOptions
2039+
)
2040+
.call(cwd = root)
2041+
expect(res.out.text().trim == msg)
2042+
}
2043+
}
19622044
}

0 commit comments

Comments
 (0)