Skip to content

Commit

Permalink
Basic ability to pass arguments to scripts from external command-line…
Browse files Browse the repository at this point in the history
…s. Not fully tested!

fixlonglines

fixbuild

try to fix build again
  • Loading branch information
Li Haoyi committed Nov 29, 2015
1 parent f0b640f commit 7606f6b
Show file tree
Hide file tree
Showing 14 changed files with 284 additions and 105 deletions.
@@ -0,0 +1,5 @@
val x = 1
import ammonite.ops._
def main(i: Int, s: String, path: Path = cwd) = {
println(s"Hello! ${s * i} ${path.relativeTo(cwd)}")
}
@@ -0,0 +1,5 @@
val x = 1

def main() = {
println("Hello! " + x)
}
2 changes: 1 addition & 1 deletion integration/src/test/scala/ammonite/integration/Main.scala
Expand Up @@ -3,7 +3,7 @@ package ammonite.integration
object Main {
def main(args: Array[String]): Unit = {
import ammonite.ops._
ammonite.repl.Repl.run(
ammonite.repl.Main.run(
predef = "import ammonite.integration.Main._",
predefFile = Some(
cwd/'shell/'src/'main/'resources/'ammonite/'shell/"example-predef-bare.scala"
Expand Down
Expand Up @@ -11,25 +11,34 @@ import utest.framework.TestSuite
* standalone executable has a pretty different classloading environment
* from the "run in SBT on raw class files" that the rest of the tests use.
*
* Need to call sbt repl/assembly beforehand to make these pass
* These are also the only tests that cover all the argument-parsing
* and configuration logic inside, which the unit tests don't cover since
* they call the REPL programmatically
*/
object StandaloneTests extends TestSuite{
// Prepare standalone executable
val scalaVersion = scala.util.Properties.versionNumberString

println("StandaloneTests")
val tests = TestSuite {
val ammVersion = ammonite.Constants.version
val executableName = s"ammonite-repl-$ammVersion-$scalaVersion"
val Seq(executable) = ls.rec! cwd |? (_.last == executableName)
val replStandaloneResources = cwd/'repl/'src/'test/'resources/'standalone
val replStandaloneResources = cwd/'integration/'src/'test/'resources/'ammonite/'integration
val shellAmmoniteResources = cwd/'shell/'src/'main/'resources/'ammonite/'shell
//use Symbol to wrap symbols with dashes.
val emptyPrefdef = shellAmmoniteResources/"empty-predef.scala"
val exampleBarePredef = shellAmmoniteResources/"example-predef-bare.scala"

//we use an empty predef file here to isolate the tests from external forces.
def exec(name: String) = (%%bash(executable, "--predef-file", emptyPrefdef,
replStandaloneResources/name)).mkString("\n")
def exec(name: String, args: String*) = (
%%bash(
executable,
"--predef-file",
emptyPrefdef,
replStandaloneResources/name,
args
)
).mkString("\n")

'hello{
val evaled = exec("Hello.scala")
Expand Down Expand Up @@ -59,5 +68,19 @@ object StandaloneTests extends TestSuite{
val output = res.mkString("\n")
assert(output == "repl/src")
}
'main{
val evaled = exec("Main.scala")
assert(evaled.contains("Hello! 1"))
}
'args{
'full{
val evaled = exec("Args.scala", "3", "Moo", (cwd/'omg/'moo).toString)
assert(evaled.contains("Hello! MooMooMoo omg/moo"))
}
'default{
val evaled = exec("Args.scala", "3", "Moo")
assert(evaled.contains("Hello! MooMooMoo "))
}
}
}
}
6 changes: 6 additions & 0 deletions readme/Footer.scalatex
Expand Up @@ -19,6 +19,12 @@

@sect{Changelog}

@sect{0.5.1}
@ul
@li
Fix performance regression causing slowness when C&Ping large snippets #274
@li
Added the ability to pass arguments to Ammonite scripts from an external command line (e.g. bash)
@sect{0.5.0}
@ul
@li
Expand Down
199 changes: 199 additions & 0 deletions repl/src/main/scala/ammonite/repl/Main.scala
@@ -0,0 +1,199 @@
package ammonite.repl

import java.io.File

import ammonite.ops._

import scala.reflect.internal.annotations.compileTimeOnly
import scala.reflect.runtime.universe.TypeTag

/**
* The various entry-points to the Ammonite repl
*/
object Main{
case class Config(predef: String = "",
predefFile: Option[Path] = None,
code: Option[String] = None,
ammoniteHome: Path = defaultAmmoniteHome,
file: Option[Path] = None,
args: Seq[String] = Vector.empty,
kwargs: Map[String, String] = Map.empty)

def defaultAmmoniteHome = Path(System.getProperty("user.home"))/".ammonite"

/**
* The command-line entry point, which does all the argument parsing before
* delegating to [[run]]
*/
def main(args: Array[String]) = {
val parser = new scopt.OptionParser[Config]("ammonite") {
head("ammonite", ammonite.Constants.version)
opt[String]('p', "predef")
.action((x, c) => c.copy(predef = x))
.text("Any commands you want to execute at the start of the REPL session")
opt[String]('f', "predef-file")
.action((x, c) => c.copy(predefFile = Some(if (x(0) == '/') Path(x) else cwd/RelPath(x))))
.text("Lets you load your predef from a custom location")
opt[String]('c', "code")
.action((x, c) => c.copy(code = Some(x)))
.text("Pass in code to be run immediately in the REPL")
opt[File]('h', "home")
.valueName("<file>")
.action((x, c) => c.copy(ammoniteHome = Path(x)))
.text("The home directory of the REPL; where it looks for config and caches")
arg[File]("<file-args>...")
.optional()
.action { (x, c) => c.copy(file = Some(Path(x))) }
.text("The Ammonite script file you want to execute")
arg[String]("<args>...")
.optional()
.unbounded()
.action { (x, c) => c.copy(args = c.args :+ x) }
.text("Any arguments you want to pass to the Ammonite script file")
}
val (before, after) = args.splitAt(args.indexOf("--") match {
case -1 => Int.MaxValue
case n => n
})
val keywordTokens = after.drop(1)
assert(
keywordTokens.length % 2 == 0,
s"""Only pairs of keyword arguments can come after `--`.
|Invalid number of tokens: ${keywordTokens.length}""".stripMargin
)

val kwargs = for(Array(k, v) <- keywordTokens.grouped(2)) yield{

assert(
k.startsWith("--") &&
scalaparse.syntax
.Identifiers
.Id
.parse(k.stripPrefix("--"))
.isInstanceOf[fastparse.core.Result.Success[_]],
s"""Only pairs of keyword arguments can come after `--`.
|Invalid keyword: $k""".stripMargin
)
(k.stripPrefix("--"), v)
}

for(c <- parser.parse(before, Config())){
run(
c.predef,
c.ammoniteHome,
c.code,
c.predefFile,
c.file,
c.args,
kwargs.toMap
)
}
}

implicit def ammoniteReplArrowBinder[T](t: (String, T))(implicit typeTag: TypeTag[T]) = {
Bind(t._1, t._2)(typeTag)
}

/**
* The debug entry-point: embed this inside any Scala program to open up
* an ammonite REPL in-line with access to that program's variables for
* inspection.
*/
def debug(replArgs: Bind[_]*): Any = {

val storage = Storage(defaultAmmoniteHome, None)
val repl = new Repl(
System.in, System.out,
storage = Ref(storage),
predef = "",
replArgs
)

repl.run()
}

/**
* The main entry-point after partial argument de-serialization.
*/
def run(predef: String = "",
ammoniteHome: Path = defaultAmmoniteHome,
code: Option[String] = None,
predefFile: Option[Path] = None,
file: Option[Path] = None,
args: Seq[String] = Vector.empty,
kwargs: Map[String, String] = Map.empty) = {

Timer("Repl.run Start")
def storage = Storage(ammoniteHome, predefFile)
lazy val repl = new Repl(
System.in, System.out,
storage = Ref(storage),
predef = predef
)
(file, code) match{
case (None, None) => println("Loading..."); repl.run()
case (Some(path), None) => runScript(repl, path, args, kwargs)
case (None, Some(code)) => repl.interp.replApi.load(code)
}
Timer("Repl.run End")
}

def runScript(repl: Repl, path: Path, args: Seq[String], kwargs: Map[String, String]): Unit = {
val imports = repl.interp.processModule(read(path))
repl.interp.init()
imports.find(_.toName == "main").foreach { i =>
val quotedArgs =
args.map(pprint.PPrinter.escape)
.map(s => s"""arg("$s")""")

val quotedKwargs =
kwargs.mapValues(pprint.PPrinter.escape)
.map { case (k, s) => s"""$k=arg("$s")""" }

repl.interp.replApi.load(s"""
|import ammonite.repl.ScriptInit.{arg, callMain, pathRead}
|callMain{
|main(${(quotedArgs ++ quotedKwargs).mkString(", ")})
|}
""".stripMargin)
}
}
}

/**
* Code used to de-serialize command-line arguments when calling an Ammonite
* script. Basically looks for a [[scopt.Read]] for the type of each argument
* and uses that to de-serialize the given [[String]] into that argument.
*
* Needs a bit of macro magic to work.
*/
object ScriptInit{
import language.experimental.macros
import reflect.macros.Context
def callMainImpl[T](c: Context)(t: c.Expr[T]): c.Expr[T] = {
import c.universe._
val apply = t.tree.asInstanceOf[Apply]
val paramSymbols = apply.symbol.typeSignature.asInstanceOf[MethodType].params
val reads = paramSymbols.zip(apply.args).map{ case (tpe, term) =>
term match{
case q"$prefix($inner)" => q"implicitly[scopt.Read[$tpe]].reads($inner)"
case x => x
}
}
c.Expr[T](q"${apply.fun}(..$reads)")
}
@compileTimeOnly("This is a marker function and should not exist after macro expansion")
def arg[T](s: String): T = ???

/**
* Takes the call to the main method, with [[arg]]s wrapping every argument,
* and converts them to the relevant [[scopt.Read]] calls to properly
* de-serialize them
*/
def callMain[T](t: T): T = macro callMainImpl[T]

/**
* Additional [[scopt.Read]] instance to teach it how to read Ammonite paths
*/
implicit def pathRead = scopt.Read.stringRead.map(Path(_))
}
79 changes: 3 additions & 76 deletions repl/src/main/scala/ammonite/repl/Repl.scala
Expand Up @@ -6,6 +6,7 @@ import acyclic.file
import ammonite.repl.interp.Interpreter

import scala.annotation.tailrec
import scala.reflect.internal.annotations.compileTimeOnly
import scala.reflect.runtime.universe.TypeTag
import ammonite.ops._
class Repl(input: InputStream,
Expand Down Expand Up @@ -111,7 +112,7 @@ object Repl{
val clsNameString = clsName.replace("$", error+"$"+highlightError)
val method =
s"$error$prefixString$highlightError$clsNameString$error" +
s".$highlightError${f.getMethodName}$error"
s".$highlightError${f.getMethodName}$error"

s"\t$method($src)"
}
Expand All @@ -127,78 +128,4 @@ object Repl{
)
traces.mkString("\n")
}
case class Config(predef: String = "",
predefFile: Option[Path] = None,
code: Option[String] = None,
ammoniteHome: Path = defaultAmmoniteHome,
file: Option[Path] = None)

def defaultAmmoniteHome = Path(System.getProperty("user.home"))/".ammonite"
def main(args: Array[String]) = {
val parser = new scopt.OptionParser[Config]("ammonite") {
head("ammonite", ammonite.Constants.version)
opt[String]('p', "predef")
.action((x, c) => c.copy(predef = x))
.text("Any commands you want to execute at the start of the REPL session")
opt[String]('f', "predef-file")
.action((x, c) => c.copy(predefFile = Some(if (x(0) == '/') Path(x) else cwd/RelPath(x))))
.text("Lets you load your predef from a custom location")
opt[String]('c', "code")
.action((x, c) => c.copy(code = Some(x)))
.text("Pass in code to be run immediately in the REPL")
opt[File]('h', "home")
.valueName("<file>")
.action((x, c) => c.copy(ammoniteHome = Path(x)))
.text("The home directory of the REPL; where it looks for config and caches")
arg[File]("<file>...")
.optional()
.action { (x, c) => c.copy(file = Some(Path(x))) }
.text("The Ammonite script file you want to execute")
}
for(c <- parser.parse(args, Config())){
run(c.predef, c.ammoniteHome, c.code, c.predefFile, c.file)
}
}

implicit def ammoniteReplArrowBinder[T](t: (String, T))(implicit typeTag: TypeTag[T]) = {
Bind(t._1, t._2)(typeTag)
}

def debug(replArgs: Bind[_]*): Any = {

def storage = Storage(defaultAmmoniteHome, None)
def repl = new Repl(
System.in, System.out,
storage = Ref(storage),
predef = "",
replArgs
)

repl.run()
}
def run(predef: String = "",
ammoniteHome: Path = defaultAmmoniteHome,
code: Option[String] = None,
predefFile: Option[Path] = None,
file: Option[Path] = None) = {

Timer("Repl.run Start")
def storage = Storage(ammoniteHome, predefFile)
lazy val repl = new Repl(
System.in, System.out,
storage = Ref(storage),
predef = predef
)
(file, code) match{
case (None, None) =>
println("Loading...")
repl.run()
case (Some(path), None) =>
repl.interp.replApi.load.module(path)
case (None, Some(code)) =>
repl.interp.replApi.load(code)
}
Timer("Repl.run End")
}
}

}

0 comments on commit 7606f6b

Please sign in to comment.