Skip to content

Commit

Permalink
Add support for execution of Scala, Java and script snippets
Browse files Browse the repository at this point in the history
  • Loading branch information
Gedochao committed Jul 12, 2022
1 parent a0ab00f commit 522ef63
Show file tree
Hide file tree
Showing 11 changed files with 444 additions and 10 deletions.
5 changes: 3 additions & 2 deletions modules/build/src/main/scala/scala/build/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ object Build {
.replace(".", "_")
.replace("/", ".")
}
case Left(stdin @ "stdin") => Some(s"${stdin}_sc")
case _ => None
case Left(virtual) if virtual == "stdin" || virtual == "snippet" =>
Some(s"${virtual}_sc")
case _ => None
}
val filteredMainClasses =
mainClasses.filter(!scriptInferredMainClasses.contains(_))
Expand Down
49 changes: 45 additions & 4 deletions modules/build/src/main/scala/scala/build/Inputs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ object Inputs {
}

sealed abstract class VirtualSourceFile extends Virtual {
def isStdin: Boolean = source.contains("<stdin>")
def isStdin: Boolean = source.contains("<stdin>")
def isSnippet: Boolean = source.contains("<snippet>")
}

sealed trait SingleFile extends OnDisk with SingleElement
Expand Down Expand Up @@ -325,6 +326,32 @@ object Inputs {
}
}

def validateSnippets(
scriptSnippetOpt: Option[String] = None,
scalaSnippetOpt: Option[String] = None,
javaSnippetOpt: Option[String] = None
): Seq[Either[String, Seq[Element]]] = {
def validateSnippet(
maybeExpression: Option[String],
f: Array[Byte] => Element
): Option[Either[String, Seq[Element]]] =
maybeExpression.filter(_.nonEmpty).map(expression =>
Right(Seq(f(expression.getBytes(StandardCharsets.UTF_8))))
)

Seq(
validateSnippet(
scriptSnippetOpt,
content => VirtualScript(content, "snippet", os.sub / "snippet.sc")
),
validateSnippet(
scalaSnippetOpt,
content => VirtualScalaFile(content, "<snippet>-scala-file")
),
validateSnippet(javaSnippetOpt, content => VirtualJavaFile(content, "<snippet>-java-file"))
).flatten
}

def validateArgs(
args: Seq[String],
cwd: os.Path,
Expand Down Expand Up @@ -382,16 +409,22 @@ object Inputs {
baseProjectName: String,
download: String => Either[String, Array[Byte]],
stdinOpt: => Option[Array[Byte]],
scriptSnippetOpt: Option[String],
scalaSnippetOpt: Option[String],
javaSnippetOpt: Option[String],
acceptFds: Boolean,
forcedWorkspace: Option[os.Path]
): Either[String, Inputs] = {
val validatedArgs: Seq[Either[String, Seq[Element]]] =
validateArgs(args, cwd, download, stdinOpt, acceptFds)
val invalid = validatedArgs.collect {
val validatedExpressions: Seq[Either[String, Seq[Element]]] =
validateSnippets(scriptSnippetOpt, scalaSnippetOpt, javaSnippetOpt)
val validatedArgsAndExprs = validatedArgs ++ validatedExpressions
val invalid = validatedArgsAndExprs.collect {
case Left(msg) => msg
}
if (invalid.isEmpty) {
val validElems = validatedArgs.collect {
val validElems = validatedArgsAndExprs.collect {
case Right(elem) => elem
}.flatten
assert(validElems.nonEmpty)
Expand All @@ -410,10 +443,15 @@ object Inputs {
defaultInputs: () => Option[Inputs] = () => None,
download: String => Either[String, Array[Byte]] = _ => Left("URL not supported"),
stdinOpt: => Option[Array[Byte]] = None,
scriptSnippetOpt: Option[String] = None,
scalaSnippetOpt: Option[String] = None,
javaSnippetOpt: Option[String] = None,
acceptFds: Boolean = false,
forcedWorkspace: Option[os.Path] = None
): Either[String, Inputs] =
if (args.isEmpty)
if (
args.isEmpty && scriptSnippetOpt.isEmpty && scalaSnippetOpt.isEmpty && javaSnippetOpt.isEmpty
)
defaultInputs().toRight(
"No inputs provided (expected files with .scala or .sc extensions, and / or directories)."
)
Expand All @@ -425,6 +463,9 @@ object Inputs {
baseProjectName,
download,
stdinOpt,
scriptSnippetOpt,
scalaSnippetOpt,
javaSnippetOpt,
acceptFds,
forcedWorkspace
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ final case class JavaPreprocessor(
case v: Inputs.VirtualJavaFile =>
val res = either {
val relPath =
if (v.isStdin) {
if (v.isStdin || v.isSnippet) {
val classNameOpt = value {
(new JavaParserProxyMaker)
.get(
Expand All @@ -76,7 +76,7 @@ final case class JavaPreprocessor(
}
val fileName = classNameOpt
.map(_ + ".java")
.getOrElse("stdin.java")
.getOrElse(if (v.isStdin) "stdin.java" else "java-snippet.java")
os.sub / fileName
}
else v.subPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ case object ScalaPreprocessor extends Preprocessor {

case v: Inputs.VirtualScalaFile =>
val res = either {
val relPath = if (v.isStdin) os.sub / "stdin.scala" else v.subPath
val relPath = v match {
case v if v.isStdin => os.sub / "stdin.scala"
case v if v.isSnippet => os.sub / "scala-snippet.scala"
case v => v.subPath
}
val content = new String(v.content, StandardCharsets.UTF_8)
val (requirements, scopedRequirements, options, updatedContentOpt) =
value(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ final case class SharedOptions(

@Group("Scala")
@HelpMessage("Show help for scalac. This is an alias for --scalac-option -help")
scalacHelp: Boolean = false,
scalacHelp: Boolean = false,

@Recurse
snippet: SnippetOptions = SnippetOptions(),

@Group("Java")
@HelpMessage("Add extra JARs in the class path")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package scala.cli.commands

import caseapp._

// format: off
final case class SnippetOptions(
@Group("Scala")
@HelpMessage("Allows to execute a passed string as a Scala script")
@Name("e")
@Name("executeScript")
@Name("executeScalaScript")
@Name("executeSc")
scriptSnippet: Option[String] = None,

@Group("Scala")
@HelpMessage("Allows to execute a passed string as Scala code")
@Name("executeScala")
scalaSnippet: Option[String] = None,

@Group("Java")
@HelpMessage("Allows to execute a passed string as Java code")
@Name("executeJava")
javaSnippet: Option[String] = None,
)
// format: on

object SnippetOptions {
implicit lazy val parser: Parser[SnippetOptions] = Parser.derive
implicit lazy val help: Help[SnippetOptions] = Help.derive
// Parser.Aux for using ExpressionOptions with @Recurse in other options
implicit lazy val parserAux: Parser.Aux[SnippetOptions, parser.D] = parser

}
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ object SharedOptionsUtil {
defaultInputs = defaultInputs,
download = downloadInputs,
stdinOpt = readStdin(logger = logger),
scriptSnippetOpt = v.snippet.scriptSnippet,
scalaSnippetOpt = v.snippet.scalaSnippet,
javaSnippetOpt = v.snippet.javaSnippet,
acceptFds = !Properties.isWin,
forcedWorkspace = workspace.forcedWorkspaceOpt
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,48 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
expect(output == expectedOutput)
}
}
test(
"snippets mixed with piped Scala code and existing sources allow for cross-references"
) {
val hello = "Hello"
val comma = ", "
val world = "World"
val exclamation = "!"
val expectedOutput = hello + comma + world + exclamation
val scriptSnippet = s"def world = \"$world\""
val scalaSnippet = "case class ScalaSnippetData(value: String)"
val javaSnippet =
s"public class JavaSnippet { public static String exclamation = \"$exclamation\"; }"
val pipedInput = s"def hello = \"$hello\""
val inputs =
TestInputs(Seq(os.rel / "Main.scala" ->
s"""object Main extends App {
| val hello = stdin.hello
| val comma = ScalaSnippetData(value = "$comma").value
| val world = snippet.world
| val exclamation = JavaSnippet.exclamation
| println(hello + comma + world + exclamation)
|}
|""".stripMargin))
inputs.fromRoot { root =>
val output =
os.proc(
TestUtil.cli,
".",
"_.sc",
"--script-snippet",
scriptSnippet,
"--scala-snippet",
scalaSnippet,
"--java-snippet",
javaSnippet,
extraOptions
)
.call(cwd = root, stdin = pipedInput)
.out.text().trim
expect(output == expectedOutput)
}
}
test("pick .scala main class over in-context scripts, including piped ones") {
val inputs = TestInputs(
Seq(
Expand Down Expand Up @@ -1916,4 +1958,41 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
expect(mainClasses == Set(scalaFile1, scalaFile2, s"$scriptsDir.${scriptName}_sc"))
}
}

test("correctly run a script snippet") {
emptyInputs.fromRoot { root =>
val msg = "123456" // FIXME: change this to a a non-numeric string when Windows encoding is handled properly
val res = os.proc(TestUtil.cli, "-e", s"println($msg)", extraOptions).call(cwd = root)
expect(res.out.text().trim == msg)
}
}

test("correctly run a scala snippet") {
emptyInputs.fromRoot { root =>
val msg = "123456" // FIXME: change this to a a non-numeric string when Windows encoding is handled properly
val res =
os.proc(
TestUtil.cli,
"--scala-snippet",
s"object Hello extends App { println($msg) }",
extraOptions
)
.call(cwd = root)
expect(res.out.text().trim == msg)
}
}

test("correctly run a java snippet") {
emptyInputs.fromRoot { root =>
val msg = "123456" // FIXME: change this to a a non-numeric string when Windows encoding is handled properly
val res = os.proc(
TestUtil.cli,
"--java-snippet",
s"public class Main { public static void main(String[] args) { System.out.println($msg); } }",
extraOptions
)
.call(cwd = root)
expect(res.out.text().trim == msg)
}
}
}
Loading

0 comments on commit 522ef63

Please sign in to comment.