Ammonite is a collection of projects, that can be used together or alone:
Depending on why you are here, click on the above links to jump straight to the documentation that interests you. For an overview of the project and it's motivation, check out this talk:
Ammonite is a cleanroom re-implementation of the Scala REPL from first principles. It is much more featureful than the default REPL and comes with a lot of ergonomic improvements and configurability that may be familiar to people coming from IDEs or other REPLs such as IPython or Zsh. It can be combined with Ammonite-Ops to replace Bash as your systems shell, but also can be used alone as a superior version of the default Scala REPL, or as a debugging tool.
If you want to use Ammonite as a plain Scala shell, download the standalone executable:
$ curl -L -o amm http://git.io/vBTzM; chmod +x amm; ./amm
This will give you access to Ammonite for Scala:
With Pretty Printing, Syntax Highlighting for input and output, Artifact Loading in-REPL, and all the other nice features!
If you want to use Ammonite as a filesystem shell, take a look at Ammonite-Shell
If you want some initialization code available to the REPL, you can add it to your ~/.ammonite/predef.scala
.
You can also try out Ammonite in an existing SBT project, add the following to your build.sbt
libraryDependencies += "com.lihaoyi" % "ammonite-repl" % "0.5.0" % "test" cross CrossVersion.full
initialCommands in (Test, console) := """ammonite.repl.Repl.run("")"""
After that, simple hit
sbt projectName/test:console
To activate the Ammonite REPL
You can also pass a string to the run
call containing any commands or imports you want executed at the start of every run. If you want Ammonite to be available in all projects, simply add the above snippet to a new file ~/.sbt/0.13/global.sbt
.
If you have any questions, come hang out on the mailing list or gitter channel and get help!
Ammonite-REPL supports many more features than the default REPL, including:
@ import scalatags.Text.all._
error: not found: value scalatags
@ load.ivy("com.lihaoyi" %% "scalatags" % "0.4.5")
@ import scalatags.Text.all._
import scalatags.Text.all._
@ a("omg", href:="www.google.com").render
res2: String = $tq
<a href="www.google.com">omg</a>
$tq
Ammonite allows you to load artifacts directly from maven central by copy & pasting their SBT ivy-coordinate-snippet. In addition, you can also load in jars as java.io.File
s to be included in the session or simple String
s to be executed using the load
command.
This makes Ammonite ideal for trying out new libraries or tools. You can pull down projects like Scalaz or Shapeless and immediately start working with them in the REPL:
@ load.ivy("org.scalaz" %% "scalaz-core" % "7.1.1")
@ import scalaz._
import scalaz._
@ import Scalaz._
import Scalaz._
@ (Option(1) |@| Option(2))(_ + _)
res3: Option[Int] = Some(3)
@ load.ivy("com.chuusai" %% "shapeless" % "2.1.0")
@ import shapeless._
@ (1 :: "lol" :: List(1, 2, 3) :: HNil)
res2: Int :: String :: List[Int] :: HNil = ::(1, ::("lol", ::(List(1, 2, 3), HNil)))
@ res2(1)
res3: String = "lol"
Even non-trivial web frameworks like Finagle or Akka-HTTP can be simply pulled down and run in the REPL!
@ load.ivy("com.twitter" %% "finagle-httpx" % "6.26.0")
@ import com.twitter.finagle._; import com.twitter.util._
@ var serverCount = 0
@ var clientResponse = 0
@ val service = new Service[httpx.Request, httpx.Response] {
@ def apply(req: httpx.Request): Future[httpx.Response] = {
@ serverCount += 1
@ Future.value(
@ httpx.Response(req.version, httpx.Status.Ok)
@ )
@ }
@ }
@ val server = Httpx.serve(":8080", service)
@ val client: Service[httpx.Request, httpx.Response] = Httpx.newService(":8080")
@ val request = httpx.Request(httpx.Method.Get, "/")
@ request.host = "www.scala-lang.org"
@ val response: Future[httpx.Response] = client(request)
@ response.onSuccess { resp: httpx.Response =>
@ clientResponse = resp.getStatusCode
@ }
@ Await.ready(response)
@ serverCount
res12: Int = 1
@ clientResponse
res13: Int = 200
@ server.close()
@ load.ivy("com.typesafe.akka" %% "akka-http-experimental" % "1.0-M3")
@ implicit val system = akka.actor.ActorSystem()
@ val serverBinding = akka.http.Http(system).bind(interface = "localhost", port = 31337)
@ implicit val materializer = akka.stream.ActorFlowMaterializer()
@ var set = false
@ serverBinding.connections.runForeach { connection =>
@ set = true
@ }
@ set
res6: Boolean = false
@ akka.stream.scaladsl.Source(
@ List(akka.http.model.HttpRequest(uri="/"))
@ ).via(
@ akka.http.Http().outgoingConnection("localhost", port=31337).flow
@ ).runForeach(println)
@ Thread.sleep(200)
@ set
res9: Boolean = true
@ system.shutdown()
@ Seq.fill(10)(Seq.fill(3)("Foo"))
res0: Seq[Seq[String]] = List(
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo"),
List("Foo", "Foo", "Foo")
)
@ case class Foo(i: Int, s0: String, s1: Seq[String])
defined class Foo
@ Foo(1, "", Nil)
res2: Foo = Foo(1, "", List())
@ Foo(
@ 1234567,
@ "I am a cow, hear me moo",
@ Seq("I weigh twice as much as you", "and I look good on the barbecue")
@ )
res3: Foo = Foo(
1234567,
"I am a cow, hear me moo",
List("I weigh twice as much as you", "and I look good on the barbecue")
)
Ammonite-REPL uses PPrint to display its output by default. That means that everything is nicely formatted to fit within the width of the terminal, and is copy-paste-able!
By default, Ammonite truncates the pretty-printed output to avoid flooding your terminal. If you want to disable truncation, call show(...)
on your expression to pretty-print it's full output. You can also pass in an optional height = ...
parameter to control how much you want to show before truncation.
@ Seq.fill(20)(100)
res0: Seq[Int] = List(
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
...
@ show(Seq.fill(20)(100))
res1: ammonite.pprint.Show[Seq[Int]] = List(
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100,
100
)
@ show(Seq.fill(20)(100), height = 3)
res2: ammonite.pprint.Show[Seq[Int]] = List(
100,
100,
...
@ pprintConfig() = pprintConfig().copy(height = 5 )
@ Seq.fill(20)(100)
res4: Seq[Int] = List(
100,
100,
100,
100,
...
Ammonite-REPL intelligently truncates your output when it's beyond a certain size. You can request for the full output to be printed on-demand, print a certain number of lines, or even change the implicit pprintConfig
so subsequent lines all use your new configuration.
Ammonite syntax highlights both the code you're entering as well as any output being echoed in response. This should make it much easier to work with larger snippets of input.
All colors are configurable, and you can easily turn off colors entirely via the Configuration.
Stack traces are similarly highlighted, for easier reading:
Ammonite allows you to save your work half way through, letting you discard and future changes and returning to the state of the world you saved.
Defined some memory-hogging variable you didn't need? Loaded the wrong version of some third-party library? Reluctant to reload the REPL because reloading is slow? Fear not! With Ammonite, you can save your important work, do whatever you want later, and simply discard all the jars you loaded, variables you defined
@ val veryImportant = 1
veryImportant: Int = 1
@ sess.save()
@ val oopsDontWantThis = 2
oopsDontWantThis: Int = 2
@ // Let's try this new cool new library
@ load.ivy("com.lihaoyi" %% "scalatags" % "0.5.3")
@ veryImportant
res4: Int = 1
@ oopsDontWantThis
res5: Int = 2
@ import scalatags.Text.all._
@ div("Hello").render
res7: String = "<div>Hello</div>"
@ // Oh no, maybe we don't want scalatags!
@ sess.load()
@ veryImportant
res9: Int = 1
@ oopsDontWantThis
error: not found: value oopsDontWantThis
@ import scalatags.Text.all._
error: not found: value scalatags
Apart from plain save
s and load
s, which simply discard everything after the most recent save, you can also provide a name to these functions. That lets you stop working on a branch, go do something else for a while, and be able to come back later to continue where you left off:
@ val (x, y) = (1, 2)
x: Int = 1
y: Int = 2
@ sess.save("xy initialized")
@ val z = x + y
z: Int = 3
@ sess.save("first z")
@ sess.load("xy initialized")
@ val z = x - y
z: Int = -1
@ sess.save("second z")
@ z
res7: Int = -1
@ sess.load("first z")
@ z
res9: Int = 3
@ sess.load("second z")
@ z
res11: Int = -1
Lastly, you have the sess.pop()
function. Without any arguments, it behaves the same as sess.load()
, reseting you to your last savepoint. However, you can pass in a number of session frames which you'd like to pop, allow you to reset your session to even earlier save points. sess.pop(2)
would put you two save-points ago, sess.pop(3)
would put you three save-points ago, letting you reach earlier save-points even if you did not give them names. Passing in a large number like sess.pop(999)
would reset your session all the way until the start.
Ammonite's save
and load
functionality is implemented via Java class-loaders.
Ammonite by default ships with a custom implementation of readline, which provides...
You can use the Up and Down arrows to navigate between lines within your snippet. Enter
only executes the code when you're on the last line of a multi-line snippet, meaning you can take your time, space out your code nicely, and fix any mistakes anywhere in your snippet. History is multi-line too, meaning re-running a multi-line snippet is trivial, even with tweaks.
Long gone are the days where you're desperately trying to cram everything in a single line, or curse quietly when you notice a mistake in an earlier line you are no longer able to fix. No more painstakingly crafting a multi-line snippet, and then having to painstakingly fish it line by individual line out of the history so you can run it again!
You can use Alt-Left/Right to move forward/backwards by one word at a time or hold down Shift to select text to delete. These compose as you'd be used to: e.g. Shift-Up selects all the text between your current cursor and the same column one row up.
Tab
and Shift-Tab
now work to block-indent and -dedent sections of code, as you'd expect in any desktop editor like Sublime Text or IntelliJ. This further enhances the multi-line editing experience, letting your nicely lay-out your more-complex REPL commands the same way you'd format code in any other editor.
All the readline-style navigation hotkeys like Ctrl-W to delete a word or Esc-Left/Right to navigate one word left/right still work. If you're comfortable with consoles like Bash, Python, IPython or even the default Scala console, you should have no trouble as all the exact same hotkeys work in Ammonite
The original Scala REPL provides no autocomplete except for the most basic scenarios of value.<complete>
. In the Ammonite-REPL, you get the same autocomplete-anywhere support that you get in a modern IDE.
@ Seq(1, 2, 3).map(x => x.)
getClass ## asInstanceOf isInstanceOf
toString hashCode equals !=
== % / *
- + ^ &
| >= > <=
< >> >>> <<
unary_- unary_+ unary_~ toDouble
toFloat toLong toInt toChar
toShort toByte compareTo doubleValue
...
@ Futu
scala.collection.parallel.FutureThreadPoolTasks
scala.collection.parallel.FutureTasks
scala.concurrent.impl.Future$PromiseCompletingRunnable
scala.concurrent.impl.Future
scala.concurrent.Future
scala.concurrent.FutureTaskRunner
scala.concurrent.Future$InternalCallbackExecutor
scala.concurrent.Future$class
java.util.concurrent.Future
java.util.concurrent.FutureTask$WaitNode
java.util.concurrent.FutureTask
com.sun.corba.se.impl.orbutil.closure.Future
Neither of these examples work in the standard Scala REPL.
@ while(true) ()
... hangs ...
^Ctrl-C
Interrupted!
@
The traditional Scala REPL doesn't handle runaway code, and gives you no option but to kill the process, losing all your work. Ammonite-REPL lets you interrupt the thread, stop the runaway-command and keep going.
@ val x = 1
x: Int = 1
@ /* trigger compiler crash */ trait Bar { super[Object].hashCode }
error: java.lang.AssertionError: assertion failed
@ 1 + x
res1: Int = 2
The default Scala REPL throws away all your work if the compiler crashes. This doesn't make any sense, because all the compiler is is a dumb String => Array[Byte]
pipe. In the Ammonite, we simply swap out the broken compiler for a new one and let you continue your work.
To enter multiline input into the Ammonite-REPL, simply wrap the multiple lines in curly braces { ... }
, and Ammonite will wait until you close it before evaluating the contents:
@ {
@ val x = 1
@ val y = 2
@ x + y
@ }
x: Int = 1
y: Int = 2
res0_2: Int = 3
As you can see, the contents of the { ... }
block are unwrapped and evaluated as top-level statements. You can use this to e.g. declare mutually recursive functions or classes & companion-objects without being forced to squeeze everything onto a single line.
If you don't want this un-wrapping behavior, simply add another set of curlies and the block will be evaluated as a normal block, to a single expression:
@ {{
@ val x = 1
@ val y = 2
@ x + y
@ }}
res0: Int = 3
Apart from the above features, the Ammonite REPL fixes a large number of bugs in the default Scala REPL, including but not limited to:
Ammonite contains a range of useful built-ins implemented as normal functions. Everything inside the ReplAPI
trait is imported by default and can be accessed directly by default to control the console.
trait ReplAPI {
/**
* Exit the Ammonite REPL. You can also use Ctrl-D to exit
*/
def exit = throw ReplExit(())
/**
* Exit the Ammonite REPL. You can also use Ctrl-D to exit
*/
def exit(value: Any) = throw ReplExit(value)
/**
* Read/writable prompt for the shell. Use this to change the
* REPL prompt at any time!
*/
val prompt: Ref[String]
/**
* The front-end REPL used to take user input. Modifiable!
*/
val frontEnd: Ref[FrontEnd]
/**
* Display help text if you don't know how to use the REPL
*/
def help: String
/**
* History of commands that have been entered into the shell, including
* previous sessions
*/
def fullHistory: History
/**
* History of commands that have been entered into the shell during the
* current session
*/
def history: History
/**
* Get the `Type` object of [[T]]. Useful for finding
* what its methods are and what you can do with it
*/
def typeOf[T: WeakTypeTag]: Type
/**
* Get the `Type` object representing the type of `t`. Useful
* for finding what its methods are and what you can do with it
*
*/
def typeOf[T: WeakTypeTag](t: => T): Type
/**
* Tools related to loading external scripts and code into the REPL
*/
def load: Load
/**
* The colors that will be used to render the Ammonite REPL in the terminal
*/
val colors: Ref[Colors]
/**
* Throw away the current scala.tools.nsc.Global and get a new one
*/
def newCompiler(): Unit
/**
* Access the compiler to do crazy things if you really want to!
*/
def compiler: scala.tools.nsc.Global
/**
* Show all the imports that are used to execute commands going forward
*/
def imports: String
/**
* Controls how things are pretty-printed in the REPL. Feel free
* to shadow this with your own definition to change how things look
*/
implicit val pprintConfig: Ref[pprint.Config]
implicit def derefPPrint(implicit t: Ref[pprint.Config]): pprint.Config = t()
/**
* Current width of the terminal
*/
def width: Int
/**
* Current height of the terminal
*/
def height: Int
def replArgs: Vector[ammonite.repl.Bind[_]]
/**
* Lets you configure the pretty-printing of a value. By default, it simply
* disables truncation and prints the entire thing, but you can set other
* parameters as well if you want.
*/
def show[T: PPrint](implicit cfg: Config): T => Unit
def show[T: PPrint](t: T,
width: Integer = 0,
height: Integer = null,
indent: Integer = null,
colors: pprint.Colors = null)
(implicit cfg: Config = Config.Defaults.PPrintConfig): Unit
/**
* Functions that can be used to manipulate the current REPL session:
* check-pointing progress, reverting to earlier checkpoints, or deleting
* checkpoints by name.
*
* Frames get pushed on a stack; by default, a saved frame is
* accessible simply by calling `load`. If you provide a name
* when `save`ing a checkpoint, it can later be `load`ed directly
* by providing the same name to `load`
*
* Un-named checkpoints are garbage collected, together with their
* classloader and associated data, when they are no longer accessible
* due to `restore`. Named checkpoints are kept forever; call `delete`
* on them if you really want them to go away.
*/
def sess: Session
}
trait Session{
/**
* The current stack of frames
*/
def frames: List[Frame]
/**
* Checkpoints your current work, placing all future work into its own
* frames. If a name is provided, it can be used to quickly recover
* that checkpoint later.
*/
def save(name: String = ""): Unit
/**
* Discards the last frames, effectively reverting your session to
* the last `save`-ed checkpoint. If a name is provided, it instead reverts
* your session to the checkpoint with that name.
*/
def load(name: String = ""): SessionChanged
/**
* Resets you to the last save point. If you pass in `num`, it resets
* you to that many savepoints since the last one.
*/
def pop(num: Int = 1): SessionChanged
/**
* Deletes a named checkpoint, allowing it to be garbage collected if it
* is no longer accessible.
*/
def delete(name: String): Unit
}
case class SessionChanged(removedImports: Set[scala.Symbol],
addedImports: Set[scala.Symbol],
removedJars: Set[java.net.URL],
addedJars: Set[java.net.URL])
object SessionChanged{
implicit val pprinter: PPrinter[SessionChanged] = PPrinter[SessionChanged]{
(data, config) =>
val output = mutable.Buffer.empty[String]
def printDelta[T: PPrint](name: String, d: Iterable[T]) = {
if (d.nonEmpty){
Iterator("\n", name, ": ") ++ pprint.tokenize(d)(implicitly, config)
}else Iterator()
}
val res = Iterator(
printDelta("Removed Imports", data.removedImports),
printDelta("Added Imports", data.addedImports),
printDelta("Removed Jars", data.removedJars),
printDelta("Added Jars", data.addedJars)
)
res.flatten
}
def delta(oldFrame: Frame, newFrame: Frame): SessionChanged = {
def frameSymbols(f: Frame) = f.previousImports.keySet.map(Symbol(_))
new SessionChanged(
frameSymbols(oldFrame) -- frameSymbols(newFrame),
frameSymbols(newFrame) -- frameSymbols(oldFrame),
oldFrame.classloader.allJars.toSet -- newFrame.classloader.allJars.toSet,
newFrame.classloader.allJars.toSet -- oldFrame.classloader.allJars.toSet
)
}
}
// End of OpsAPI
trait LoadJar {
/**
* Load a `.jar` file
*/
def jar(jar: Path): Unit
/**
* Load a library from its maven/ivy coordinates
*/
def ivy(coordinates: (String, String, String), verbose: Boolean = true): Unit
}
trait Load extends (String => Unit) with LoadJar{
/**
* Loads a command into the REPL and
* evaluates them one after another
*/
def apply(line: String): Unit
/**
* Loads and executes the scriptfile on the specified path.
* Compilation units separated by `@\n` are evaluated sequentially.
* If an error happens it prints an error message to the console.
*/
def exec(path: Path): Unit
def module(path: Path): Unit
def plugin: LoadJar
}
Ammonite defines a format that allows you to load external scripts into the REPL; this can be used to save common functionality so it can be used at a later date. In the simplest case, a script file is simply a sequence of Scala statements, e.g.
// script.scala
// print banner
println("Welcome to the XYZ custom REPL!!")
// common imports
import sys.process._
import collection.mutable
// common initialization code
val x = 123
...
Which you can then load into the REPL as desired:
@ mutable.Seq(x) // doesn't work
Compilation Failed
Main.scala:122: not found: value mutable
mutable.Seq(x) // doesn't work
^
Main.scala:122: not found: value x
mutable.Seq(x) // doesn't work
^
@ import ammonite.ops._
@ load.module("script.scala")
Welcome to the XYZ custom REPL!!
@ mutable.Seq(x) // works
res1: mutable.Seq[Int] = ArrayBuffer(123)
By default, everything in a script is compiled and executed as a single block. That means that if you want to perform classpath-modifying operations, such as load.jar
or load.ivy
, its results will not be available within the same script if you want to use methods, values or packages defined in the loaded code. To make this work, break the script up into multiple compilation units with an @ sign, e.g.
// print banner
println("Welcome to the XYZ custom REPL!!")
load.ivy("org.scalaz" %% "scalaz-core" % "7.1.1")
@
// common imports
import scalaz._
import Scalaz._
// common initialization code
...
Ammonite provides two ways to load scripts, load.exec
and load.module
.
With load.exec
the script is executed like it was pasted in the REPL. Exec scripts can access all values previously defined in the REPL, and all side-effects are guaranteed to be applied. This is useful for one-off sets of commands.
With load.module
, the script is loaded like a Scala module. That means it can't access values previously defined in the REPL, but it is guaranteed to only execute once even if loaded many times by different scripts. If you want to execute the script code multiple times, put it in a function and call it after you load the script.
Any scripts you load can themselves load scripts. You can also run scripts using the Ammonite executable from an external shell (e.g. bash):
bash$ ./amm path/to/script.scala
All types, values and imports defined in scripts are available to commands entered in REPL after loading the script.
Ammonite is configured via Scala code, that can live in the ~/.ammonite/predef.scala
file, passed in through SBT's initialCommands
, or passed to the command-line executable as --predef='...'
.
Anything that you put in predef.scala
will be executed when you load the Ammonite REPL. This is a handy place to put common imports, setup code, or even call load.ivy
to load third-party jars. The compilation of the predef is cached, so after the first run it should not noticeably slow down the initialization of your REPL.
Some examples of things you can configure:
@ // Set the shell prompt to be something else
@ repl.prompt() = ">"
@ // Change the terminal front end; the default is
@ // Ammonite on Linux/OSX and JLineWindows on Windows
@ repl.frontEnd() = ammonite.repl.frontend.FrontEnd.JLineUnix
@ repl.frontEnd() = ammonite.repl.frontend.FrontEnd.JLineWindows
@ repl.frontEnd() = ammonite.repl.frontend.AmmoniteFrontEnd()
@ // Changing the colors used by Ammonite; all at once:
@ repl.colors() = ammonite.repl.Colors.BlackWhite
@ repl.colors() = ammonite.repl.Colors.Default
@ // or one at a time:
@ repl.colors().prompt() = Console.RED
@ repl.colors().ident() = Console.GREEN
@ repl.colors().`type`() = Console.YELLOW
@ repl.colors().literal() = Console.MAGENTA
@ repl.colors().prefix() = Console.CYAN
@ repl.colors().comment() = Console.RED
@ repl.colors().keyword() = Console.BOLD
@ repl.colors().selected() = Console.UNDERLINED
@ repl.colors().error() = Console.YELLOW
By default, all the values you're seeing here with the ()
after them are Ref
s, defined as
trait StableRef[T]{
/**
* Get the current value of the this [[StableRef]] at this instant in time
*/
def apply(): T
/**
* Set the value of this [[StableRef]] to always be the value `t`
*/
def update(t: T): Unit
}
trait Ref[T] extends StableRef[T]{
/**
* Return a function that can be used to get the value of this [[Ref]]
* at any point in time
*/
def live(): () => T
/**
* Set the value of this [[Ref]] to always be the value of the by-name
* argument `t`, at any point in time
*/
def bind(t: => T): Unit
}
As you can see from the signature, you can basically interact with the Ref
s in two ways: either getting or setting their values as values, or binding their values to expressions that will be evaluated every time the Ref
's value is needed.
As an example of the latter, you can use bind
to set your prompt
to always include your current working directory
repl.prompt.bind(wd.toString + "@ ")
As is common practice in other shells. Further modifications to make it include e.g. your current branch in Git (which you can call through Ammonite's subprocess API or the current timestamp/user are similarly possible.
Apart from configuration of the rest of the shell through Refs, configuration of the Scala compiler takes place separately through the compiler's own configuration mechanism. You have access to the compiler as compiler
, and can modify its settings as you see fit. Here's an example of this in action:
@ List(1, 2, 3) + "lol"
res0: String = "List(1, 2, 3)lol"
@ compiler.settings.noimports.value = true
@ List(1, 2, 3) + "lol" // predef imports disappear
error: not found: value List
@ compiler.settings.noimports.value = false
@ List(1, 2, 3) + "lol"
res3: String = "List(1, 2, 3)lol"
@ object X extends Dynamic
error: extension of type scala.Dynamic needs to be enabled
@ compiler.settings.language.tryToSet(List("dynamics"))
@ object X extends Dynamic
defined object X
@ 1 + 1 // other things still work
If you want these changes to always be present, place them in your ~/.ammonite/predef.scala
.
Ammonite can be used as a tool to debug any other Scala program, by conveniently opening a REPL at any point within your program with which you can interact with live program data, similar to pdb/ipdb in Python. To do so, first add Ammonite to your classpath, e.g. through this SBT snippet:
libraryDependencies += "com.lihaoyi" % "ammonite-repl" % "0.5.0" cross CrossVersion.full
Note that unlike the snippet given above, we leave out the % "test"
because we may want ammonite to be available within the "main" project, and not just in the unit tests. Then, anywhere within your program, you can place a breakpoint via:
import ammonite.repl.Repl._
debug("name1" -> value1, "name2" -> value2, ...)
And when your program reaches that point, it will pause and open up an Ammonite REPL with the values you provided it bound to the names you gave it. From there, you can interact with those values as normal Scala values within the REPL. Use Ctrl-D
or exit
to exit the REPL and continue normal program execution. Note that the names given must be plain Scala identifiers.
Here's an example of it being used to debug changes to the WootJS webserver:
In this case, we added the debug
statement within the websocket frame handler, so we can inspect the values that are taking part in the client-server data exchange. You can also put the debug
statement inside a conditional, to make it break only when certain interesting situations (e.g. bugs) occur.
As you can see, you can bind the values you're interested in to names inside the debug REPL, and once in the REPL are free to explore them interactively.
The debug()
call returns : Any
; by default, this is (): Unit
, but you can also return custom values by passing in an argument to exit(...)
when you exit the REPL. This value will then be returned from debug()
, and can be used in the rest of your Scala application.
Ammonite can also be used to remotely connect to your running application and interact with it in real-time, similar to Erlang's erl -remsh
command.
This is useful if e.g. you have multiple Scala/Java processes running but aren't sure when/if you'd want to inspect them for debugging, and if so which ones. With Ammonite, you can leave a ssh server running in each process. You can then and connect-to/disconnect-from each one at your leisure, working with the in-process Scala/Java objects and methods and classes interactively, without having to change code and restart the process to add breakpoints or instrumentation.
To do this, add ammonite-sshd to your classpath, for example with SBT:
libraryDependencies += "com.lihaoyi" % "ammonite-sshd" % "0.5.0" cross CrossVersion.full
Now add repl server to your application:
import ammonite.sshd._
val replServer = new SshdRepl(
SshServerConfig(
address = "localhost", // or "0.0.0.0" for public-facing shells
port = 22222, // Any available port
username = "repl", // Arbitrary
password = "your_secure_password" // or ""
)
)
replServer.start()
And start your application. You will be able to connect to it using ssh like this: ssh repl@localhost -p22222
and interact with your running app. Invoke stop()
method whenever you want to shutdown ammonite sshd server. Here for example sshd repl server is embedded in the Akka HTTP microservice example:
Here we can interact with code live, inspecting values or calling methods on the running system. We can try different things, see which works and which not, and then put our final bits in application code. In this example app is located on local machine, but you are free to connect to any remote node running your code.
Security notes: It is probably unsafe to run this server publicly (on host "0.0.0.0"
) in a production, public-facing application. Currently it doesn't supports key-based auth, and password-based auth is notoriously weak.
Despite this, it is perfectly possible to run these on production infrastructure: simply leave the host
set to "localhost"
, and rely on the machine's own SSH access to keep out unwanted users: you would first ssh
onto the machine itself, and then ssh
into the Ammonite REPL running on localhost
.
Typically most organizations already have bastions, firewalls, and other necessary infrastructure to allow trusted parties SSH access to the relevant machines. Running on localhost
lets you leverage that and gain all the same security properties without having to re-implement them in Scala.
Ammonite-Ops is a library to make common filesystem operations in Scala as concise and easy-to-use as from the Bash shell, while being robust enough to use in large applications without getting messy. It lives in the same repo as the Ammonite REPL, but can easily be used stand-alone in a normal SBT/maven project.
To get started with Ammonite-Ops, add this to your build.sbt
:
libraryDependencies += "com.lihaoyi" %% "ammonite-ops" % "0.5.0"
And you're all set! Here's an example of some common operations you can do with Ammonite-Ops
import ammonite.ops._
// Pick the directory you want to work with,
// relative to the process working dir
val wd = cwd/'ops/'target/"scala-2.11"/"test-classes"/'example2
// Delete a file or folder, if it exists
rm! wd
// Make a folder named "folder"
mkdir! wd/'folder
// Copy a file or folder
cp(wd/'folder, wd/'folder1)
// List the current directory
val listed = ls! wd
// Write to a file without pain! Necessary
// enclosing directories are created automatically
write(wd/'dir2/"file1.scala", "package example\nclass Foo{}")
write(wd/'dir2/"file2.scala", "package example\nclass Bar{}")
// Rename all .scala files inside the folder d into .java files
ls! wd/'dir2 | mv{case r"$x.scala" => s"$x.java"}
// List files in a folder
val renamed = ls! wd/'dir2
// Line-count of all .java files recursively in wd
val lineCount = ls.rec! wd |? (_.ext == "java") | read.lines | (_.size) sum
// Find and concatenate all .java files directly in the working directory
ls! wd/'dir2 |? (_.ext == "java") | read |> write! wd/'target/"bundled.java"
These examples make heavy use of Ammonite-Ops' Paths, Operations and Extensions to achieve their minimal, concise syntax
As you can see, Ammonite-Ops replaces the common mess of boilerplate:
def removeAll(path: String) = {
def getRecursively(f: java.io.File): Seq[java.io.File] = {
f.listFiles.filter(_.isDirectory).flatMap(getRecursively) ++ f.listFiles
}
getRecursively(new java.io.File(path)).foreach{f =>
println(f)
if (!f.delete())
throw new RuntimeException("Failed to delete " + f.getAbsolutePath)
}
new java.io.File(path).delete
}
removeAll("target/folder/thing")
With a single, sleek expression:
rm! cwd/'target/'folder/'thing
That handles the common case for you: recursively deleting folders, not-failing if the file doesn't exist, etc.
Ammonite uses strongly-typed data-structures to represent filesystem paths. The two basic versions are:
Path
: an absolute path, starting from the root
RelPath
: a relative path, not rooted anywhere
Generally, almost all commands take absolute Path
s. These are basically defined as:
case class Path(segments: Seq[String]) extends BasePathImpl[Path]{
With a number of useful operations that can be performed on them. Absolute paths can be created in a few ways:
// Get the process' Current Working Directory. As a convention
// the directory that "this" code cares about (which may differ
// from the cwd) is called `wd`
val wd = cwd
// A path nested inside `wd`
wd/'folder/'file
// A path starting from the root
root/'folder/'file
// A path with spaces or other special characters
wd/"My Folder"/"My File.txt"
// Up one level from the wd
wd/up
// Up two levels from the wd
wd/up/up
Note that there are no in-built operations to change the `cwd`. In general you should not need to: simply defining a new path, e.g.
val target = cwd/'target
Should be sufficient for most needs.
Above, we made use of the cwd
built-in path. There are a number of Path
s built into Ammonite:
cwd
: The current working directory of the process. This can't be changed in Java, so if you need another path to work with the convention is to define a wd
variable.root
: The root of the filesystem.home
: The home directory of the current user.makeTmp
: Creates a temporary folder and returns the path.
RelPath
s represent relative paths. These are basically defined as:
case class RelPath(segments: Seq[String], ups: Int) extends BasePathImpl[RelPath]{
The same data structure as Path
s, except that they can represent a number of up
s before the relative path is applied. They can be created in the following ways:
// The path "folder/file"
val rel1 = 'folder/'file
val rel2 = 'folder/'file
// The path "file"; will get converted to a RelPath by an implicit
val rel3 = 'file
// The relative difference between two paths
val target = cwd/'target/'file
assert((target relativeTo cwd) == 'target/'file)
// `up`s get resolved automatically
val minus = cwd relativeTo target
val ups = up/up
assert(minus == ups)
In general, very few APIs take relative paths. Their main purpose is to be combined with absolute paths in order to create new absolute paths. e.g.
val target = cwd/'target/'file
val rel = target relativeTo cwd
val newBase = root/'code/'server
assert(newBase/rel == root/'code/'server/'target/'file)
up
is a relative path that comes in-built:
val target = root/'target/'file
assert(target/up == root/'target)
Note that all paths, both relative and absolute, are always expressed in a canonical manner:
assert((root/'folder/'file/up).toString == "/folder")
// not "/folder/file/.."
assert(('folder/'file/up).toString == "folder")
// not "folder/file/.."
So you don't need to worry about canonicalizing your paths before comparing them for equality or otherwise manipulating them.
Ammonite's paths are transparent data-structures, and you can always access the segments
and ups
directly. Nevertheless, Ammonite defines a number of useful operations that handle the common cases of dealing with these paths:
trait BasePath[ThisType <: BasePath[ThisType]]{
/**
* The individual path segments of this path.
*/
def segments: Seq[String]
/**
* Combines this path with the given relative path, returning
* a path of the same type as this one (e.g. `Path` returns `Path`,
* `RelPath` returns `RelPath`
*/
def /(subpath: RelPath): ThisType
/**
* Relativizes this path with the given `base` path, finding a
* relative path `p` such that base/p == this.
*
* Note that you can only relativize paths of the same type, e.g.
* `Path` & `Path` or `RelPath` & `RelPath`. In the case of `RelPath`,
* this can throw a [[PathError.NoRelativePath]] if there is no
* relative path that satisfies the above requirement in the general
* case.
*/
def relativeTo(target: ThisType): RelPath
/**
* This path starts with the target path, including if it's identical
*/
def startsWith(target: ThisType): Boolean
/**
* The last segment in this path. Very commonly used, e.g. it
* represents the name of the file/folder in filesystem paths
*/
def last: String
}
In this definition, ThisType
represents the same type as the current path; e.g. a Path
's /
returns a Path
while a RelPath
's /
returns a RelPath
. Similarly, you can only compare or subtract paths of the same type.
Apart from RelPath
s themselves, a number of other data structures are convertible into RelPath
s when spliced into a path using /
:
String
sSymbols
sArray[T]
s where T
is convertible into a RelPath
Seq[T]
s where T
is convertible into a RelPath
Paths not aren't interesting on their own, but serve as a base to use to perform filesystem operations in a concise and easy to use way. Here is a quick tour of the core capabilities that Ammonite-Ops provides:
import ammonite.ops._
// Let's pick our working directory
val wd: Path = cwd/'ops/'target/"scala-2.11"/"test-classes"/'example3
// And make sure it's empty
rm! wd
mkdir! wd
// Reading and writing to files is done through the read! and write!
// You can write `Strings`, `Traversable[String]`s or `Array[Byte]`s
write(wd/"file1.txt", "I am cow")
write(wd/"file2.txt", Seq("I am cow", "hear me moo"))
write(wd/'file3, "I weigh twice as much as you".getBytes)
// When reading, you can either `read!` a `String`, `read.lines!` to
// get a `Vector[String]` or `read.bytes` to get an `Array[Byte]`
read! wd/"file1.txt" ==> "I am cow"
read! wd/"file2.txt" ==> "I am cow\nhear me moo"
read.lines! wd/"file2.txt" ==> Vector("I am cow", "hear me moo")
read.bytes! wd/"file3" ==> "I weigh twice as much as you".getBytes
// These operations are mirrored in `read.resource`,
// `read.resource.lines` and `read.resource.bytes` to conveniently read
// files from your classpath:
val resourcePath = root/'testdata/"File.txt"
read.resource(resourcePath).length ==> 81
read.resource.bytes(resourcePath).length ==> 81
read.resource.lines(resourcePath).length ==> 4
// By default, `write` fails if there is already a file in place. Use
// `write.append` or `write.over` if you want to append-to/overwrite
// any existing files
write.append(wd/"file1.txt", "\nI eat grass")
write.over(wd/"file2.txt", "I am cow\nHere I stand")
read! wd/"file1.txt" ==> "I am cow\nI eat grass"
read! wd/"file2.txt" ==> "I am cow\nHere I stand"
// You can create folders through `mkdir!`. This behaves the same as
// `mkdir -p` in Bash, and creates and parents necessary
val deep = wd/'this/'is/'very/'deep
mkdir! deep
// Writing to a file also creates neccessary parents
write(deep/'deeeep/"file.txt", "I am cow")
// `ls` provides a listing of every direct child of the given folder.
// Both files and folders are included
ls! wd ==> Seq(wd/"file1.txt", wd/"file2.txt", wd/'file3, wd/'this)
// `ls.rec` does the same thing recursively
ls.rec! deep ==> Seq(deep/'deeeep, deep/'deeeep/"file.txt")
// You can move files or folders with `mv` and remove them with `rm!`
ls! deep ==> Seq(deep/'deeeep)
mv(deep/'deeeep, deep/'renamed_deeeep)
ls! deep ==> Seq(deep/'renamed_deeeep)
// `rm!` behaves the same as `rm -rf` in Bash, and deletes anything:
// file, folder, even a folder filled with contents
rm! deep/'renamed_deeeep
ls! deep ==> Seq()
// You can stat paths to find out information about any file or
// folder that exists there
val info = stat! wd/"file1.txt"
info.isDir ==> false
info.isFile ==> true
info.size ==> 20
info.name ==> "file1.txt"
// Ammonite provides an implicit conversion from `Path` to
// `stat`, so you can use these attributes directly
(wd/"file1.txt").size ==> 20
// You can also use `stat.full` which provides more information
val fullInfo = stat.full(wd/"file1.txt")
fullInfo.ctime: FileTime
fullInfo.atime: FileTime
fullInfo.group: GroupPrincipal
In these definitions, Op1
and Op2
are isomorphic to Function1
and Function2
. The main difference is that ops can be called in two ways:
rm(filepath)
rm! filepath
The latter syntax allows you to use it more easily from the command line, where remembering to close all your parenthesis is a hassle. Indentation signifies nesting, e.g. in addition to write!
you also have write.append!
and write.over!
All of these operations are pre-defined and strongly typed, so feel free to jump to their implementation to look at what they do or what else is available.
In general, each operator has sensible/safe defaults:
rm
and cp
are recursiverm
ignores the file if it doesn't existmkdir
, write
, mv
) automatically create any necessary parent directorieswrite
also does not stomp over existing files by default. You need to use write.over
In general, this should make these operations much easier to use; the defaults should cover the 99% use case without needing any special flags or fiddling.
Ammonite-Ops contains a set of extension methods on common types, which serve no purpose other than to make things more concise. These turn Scala from a "relatively-concise" language into one as tight as Bash scripts, while still maintaining the high level of type-safety and maintainability that comes with Scala code.
These extensions apply to any Traversable
: Seq
s, List
s, Array
s, and others.
things | f
is an alias for things map f
things || f
is an alias for things flatMap f
things |? f
is an alias for things filter f
things |& f
is an alias for things reduce f
things |! f
is an alias for things foreach f
These should behave exactly the same as their implementations; their sole purpose is to make things more concise at the command-line.
thing |> f
is an alias for f(thing)
This lets you flip around the function and argument, and fits nicely into the Ammonite's |
pipelines.
f! thing
is an alias for f(thing)
This is another syntax-saving extension, that makes it easy to call functions without having to constantly be opening and closing brackets. It does nothing else.
The real value of Ammonite is the fact that you can pipe things together as easily as you could in Bash. No longer do you need to write reams of boilerplate. to accomplish simple tasks. Some of these chains are listed at the top of this readme, here are a few more fun examples:
// Move all files inside the "py" folder out of it
ls! wd/"py" | mv.all*{case d/"py"/x => d/x }
// Find all dot-files in the current folder
val dots = ls! wd |? (_.last(0) == '.')
// Find the names of the 10 largest files in the current working directory
ls.rec! wd | (x => x.size -> x) sortBy (-_._1) take 10
// Sorted list of the most common words in your .scala source files
def txt = ls.rec! wd |? (_.ext == "scala") | read
def freq(s: Seq[String]) = s groupBy (x => x) mapValues (_.length) toSeq
val map = txt || (_.split("[^a-zA-Z0-9_]")) |> freq sortBy (-_._2)
As you can see, you can often compose elaborate operations entirely naturally using the available pipes, without needing to remember any special flags or techniques.
Here's another example:
// Ensure that we don't have any Scala files in the current working directory
// which have lines more than 100 characters long, excluding generated sources
// in `src_managed` folders.
def longLines(p: Path) =
(p, read.lines(p).zipWithIndex |? (_._1.length > 100) | (_._2))
val filesWithTooLongLines = (
ls.rec! cwd |? (_.ext == "scala")
| longLines
|? (_._2.length > 0)
|? (!_._1.segments.contains("src_managed"))
)
assert(filesWithTooLongLines.length == 0)
Ammonite-Ops provides easy syntax for anyone who wants to spawn sub-processes, e.g. commands like ls
or git commit -am "wip"
. This is provided through the %
and %%
operators, which are used as follows:
@ import ammonite.ops._
@ import ammonite.ops.ImplicitWd._
@ %ls
build.sbt log ops readme repl terminal
echo modules project readme.md target shell
res2: Int = 0
@ %%ls
res3: CommandResult =
build.sbt
echo
log
modules
ops
project
readme
readme.md
repl
target
terminal
...
In short, %
lets you run a command as you would in bash, and dumps the output to standard-out in a similar way, returning the return-code. This lets you run git
commands, edit files via vim
, open ssh
sessions or even start SBT
or Python
shells right from your Scala REPL!
%%
on the other hand is intended for programmatic usage: rather than printing to stdout, it returns a CommandResult
, which is simply a Stream[String]
of the lines from the subprocess. %%
throws an exception if the return-code is non-zero. The returned Stream[String]
can then be used however you like.
You can also use backticks to execute commands which aren't valid Scala identifiers, e.g.
@ %`ssh-add`
Enter passphrase for /Users/haoyi/.ssh/id_rsa:
Lastly, you can also pass arguments into these subprocess calls, as Strings, Symbols or Seqs of Strings:
@ %git 'branch
gh-pages
history
* master
speedip
res4: Int = 0
@ %%git 'branch
res5: CommandResult =
gh-pages
history
* master
speedip
@ %%git('checkout, "master")
Already on 'master'
res6: CommandResult =
M readme/Index.scalatex
Your branch is up-to-date with 'origin/master'.
@ %git("checkout", 'master)
M readme/Index.scalatex
Already on 'master'
Your branch is up-to-date with 'origin/master'.
res8: Int = 0
@ val stuff = List("readme.md", "build.sbt")
stuff: List[String] = List("readme.md", "build.sbt")
@ %ls(".gitignore", stuff)
.gitignore build.sbt readme.md
Ammonite-Ops currently does not provide many convenient ways of piping together multiple processes, but support may come in future if someone finds it useful enough to implement.
%
calls subprocesses in a way that is compatible with a normal terminal. That means you can easily call things like %vim
to open a text editor, %python
to open up a Python terminal, or %sbt
to open up the SBT prompt!
@ %python
Python 2.7.6 (default, Sep 9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> print "Hello %s%s" % ("World", "!"*3)
Hello World!!!
>>> ^D
res3: Int = 0
@ %sbt
[info] Loading global plugins from /Users/haoyi/.sbt/0.13/plugins
[info] Updating {file:/Users/haoyi/.sbt/0.13/plugins/}global-plugins...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Set current project to haoyi (in build file:/Users/haoyi/)
>
%%
does not do this.
Ammonite lets you pass in environment variables to subprocess calls; just pass them in as named arguments when you invoke the subprocess ia %
or %%
:
val res0 = %%bash("-c", "echo \"Hello$ENV_ARG\"", ENV_ARG=12)
assert(res0 == Seq("Hello12"))
val res1 = %%bash("-c", "echo \"Hello$ENV_ARG\"", ENV_ARG=12)
assert(res1 == Seq("Hello12"))
val res2 = %%bash("-c", "echo 'Hello$ENV_ARG'", ENV_ARG=12)
assert(res2 == Seq("Hello$ENV_ARG"))
val res3 = %%bash("-c", "echo 'Hello'$ENV_ARG", ENV_ARG=123)
assert(res3 == Seq("Hello123"))
You can invoke files on disk using %
and %%
the same way you can invoke shell commands:
val res: CommandResult =
%%(root/'bin/'bash, "-c", "echo 'Hello'$ENV_ARG", ENV_ARG=123)
assert(res.mkString == "Hello123")
In Ammonite the current working directory is not a side-effect unlike in bash. Instead it is an argument to the command you are invoking. It can be passed in explicitly or implicitly.
val res1 = %.ls()(cwd) // explicitly
// or implicitly
import ammonite.ops.ImplicitWd._
val res2 = %ls
Note how passing it inexplicitly, you need to use a .
before the command-name in order for it to parse properly. That's a limitation of the Scala syntax that isn't likely to change. Another limitation is that when invoking a file, you need to call .apply
explicitly rather than relying on the plain-function-call syntax:
val output = %%.apply(scriptFolder/'echo_with_wd, 'HELLO)(root/'usr)
assert(output == Seq("HELLO /usr"))
Ammonite-Shell is a rock-solid system shell that can replace Bash as the interface to your operating system, using Scala as the primary command and scripting language, running on the JVM. Apart from system operations, Ammonite-Shell provides the full-range of Java APIs for usage at the command-line, including loading libraries from Maven Central.
Why would you want to use Ammonite-Shell instead of Bash? Possible reasons include:
-nrk 7
to sort by file size!"If none of these apply to you, then likely you won't be interested. If any of these bullet points strikes a chord, then read on to get started. For more discussion about why this project exists, take a look at the presentation slides for Beyond Bash: shell scripting in a typed, OO language, presented at Scala by the Bay 2015, or check out the section on Design Decisions & Tradeoffs.
To begin using Ammonite-Shell, simply download the default predef.scala
to configure your REPL to be a usable systems shell before downloading the Ammonite-REPL executable (below):
$ mkdir ~/.ammonite; curl -L -o ~/.ammonite/predef.scala http://git.io/vBTz7
$ curl -L -o amm http://git.io/vBTzM; chmod +x amm; ./amm
You can then start using Ammonite as a replacement for Bash:
Ammonite-Shell isn't backwards compatible with Bash. It isn't even the same language, giving you access to all of Scala instead of the quirky Bash scripting language. Nevertheless, lots of things you'd expect in Bash turn up in Ammonite-Shell:
bash$ pwd /Users/haoyi/Dropbox (Personal)/Workspace/Ammonite
haoyi-Ammonite@ wd res0: Path = root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite
Bash's pwd
is instead called wd
. Instead of being a subprocess that prints to stdout, wd
is simply a variable holding the working directory.
As you can see, the path syntax is also different: as an absolute path, wd
must start from root
and the path segments must be quoted as Scala "string"
s or 'symbol
s. Apart from that, however, it is basically the same. The documentation about Paths goes over the syntax and semantics of Paths in more detail.
You can navigate around the filesystem using cd!
, instead of Bash's cd
:
bash$ pwd /Users/haoyi/Dropbox (Personal)/Workspace/Ammonite bash$ cd target bash$ pwd /Users/haoyi/Dropbox (Personal)/Workspace/Ammonite/target bash$ cd .. bash$ pwd /Users/haoyi/Dropbox (Personal)/Workspace/Ammonite
haoyi-Ammonite@ wd res0: Path = root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite haoyi-Ammonite@ cd! 'target res1: Path = root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite/'target haoyi-target@ wd res2: Path = root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite/'target haoyi-target@ cd! up res3: Path = root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite haoyi-Ammonite@ wd res4: Path = root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite
bash$ ls LICENSE build.sbt integration ops project readme readme.md repl shell sshd target temp.html terminal
haoyi-Ammonite@ ls! res0: LsSeq = ".git" 'integration 'shell ".gitignore" 'ops 'sshd ".idea" 'project 'target ".travis.yml" 'readme "temp.html" 'LICENSE "readme.md" 'terminal "build.sbt" 'repl
Bash's ls
syntax is tweaked slightly to become ls!
. Apart from that, it basically does the same thing.
Listing files in other folders behaves similarly:
bash$ ls project Constants.scala build.properties build.sbt project target
haoyi-Ammonite@ ls! 'project res0: LsSeq = "Constants.scala" 'project "build.properties" 'target "build.sbt"
bash$ ls project/target config-classes resolution-cache scala-2.10 streams
haoyi-Ammonite@ ls! 'project/'target res0: LsSeq = "config-classes" "scala-2.10" "resolution-cache" 'streams
Again, we have to use the quoted 'symbol
/"string"
syntax when defining Paths, but otherwise it behaves identically. You can press <tab>
at any point after a /
or halfway through a file-name to auto-complete it, just like in Bash.
Listing recursively is done via ls.rec
, instead of find
:
bash$ find ops/src/main ops/src/main ops/src/main/scala ops/src/main/scala/ammonite ops/src/main/scala/ammonite/ops ops/src/main/scala/ammonite/ops/Extensions.scala ops/src/main/scala/ammonite/ops/FileOps.scala ops/src/main/scala/ammonite/ops/Model.scala ops/src/main/scala/ammonite/ops/package.scala ops/src/main/scala/ammonite/ops/Path.scala ops/src/main/scala/ammonite/ops/Shellout.scala
haoyi-Ammonite@ ls.rec! 'ops/'src/'main res0: LsSeq = 'scala 'scala/'ammonite 'scala/'ammonite/'ops 'scala/'ammonite/'ops/"Extensions.scala" 'scala/'ammonite/'ops/"FileOps.scala" 'scala/'ammonite/'ops/"Model.scala" 'scala/'ammonite/'ops/"Path.scala" 'scala/'ammonite/'ops/"Shellout.scala" 'scala/'ammonite/'ops/"package.scala"
ls
, ls.rec
and other commands are all functions defined by Ammonite-Ops.
Ammonite-Shell uses Ammonite-Ops to provide a nice API to use filesystem operations. The default setup will import ammonite.ops._
into your Ammonite-REPL, gives the nice path-completion shown above, and also provides some additional command-line-friendly functionality on top of the default Ammonite-Ops commands:
bash$ mkdir target/test bash$ echo "hello" > target/test/hello.txt bash$ cat target/test/hello.txt hello bash$ ls target/test hello.txt bash$ cp target/test/hello.txt target/test/hello2.txt bash$ ls target/test hello.txt hello2.txt bash$ mv target/test/hello.txt target/test/hello3.txt bash$ ls target/test hello2.txt hello3.txt bash$ rm -rf target/test
haoyi-Ammonite@ mkdir! 'target/'test haoyi-Ammonite@ write('target/'test/"hello.txt", "hello") haoyi-Ammonite@ ls! 'target/'test res2: LsSeq = "hello.txt" haoyi-Ammonite@ cp('target/'test/"hello.txt", 'target/'test/"hello2.txt") haoyi-Ammonite@ ls! 'target/'test res4: LsSeq = "hello.txt" "hello2.txt" haoyi-Ammonite@ mv('target/'test/"hello.txt", 'target/'test/"hello3.txt") haoyi-Ammonite@ ls! 'target/'test res6: LsSeq = "hello2.txt" "hello3.txt" haoyi-Ammonite@ rm! 'target/'test
Ammonite allows piping similar to how Bash does it. Unlike Bash, Ammonite has a variety of pipes you can use that do different things:
things | f
is an alias for things map f
things || f
is an alias for things flatMap f
things |? f
is an alias for things filter f
things |& f
is an alias for things reduce f
things |! f
is an alias for things foreach f
For example, this is how you can get the dot-files in the current directory:
bash$ ls -a | grep "^\." . .. .git .gitignore .idea .travis.yml
haoyi-Ammonite@ ls! cwd |? (_.last(0) == '.') res0: Seq[Path] = List( root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite/".git", root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite/".gitignore", root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite/".idea", root/'Users/'haoyi/"Dropbox (Personal)"/'Workspace/'Ammonite/".travis.yml" )
Here, we're using the |?
pipe, which basically performs a filter on the paths coming in on the left. In this case, we're checking that for each path, the first character of the last segment of that path is the character '.'
. This is slightly more verbose than Bash the bash equivalent shown above, but not by too much.
Here is how to find the largest 3 files in a given directory tree:
bash$ find ./repl/src -ls | sort -nrk 7 | head -3 128775444 56 -rw-r--r-- 1 haoyi 849048494 28167 Nov 21 02:47 ./repl/src/test/scala/ammonite/repl/session/EulerTests.scala 129417708 24 -rw-r--r-- 1 haoyi 849048494 11686 Nov 23 15:29 ./repl/src/main/scala/ammonite/repl/interp/Evaluator.scala 129417709 24 -rw-r--r-- 1 haoyi 849048494 11022 Nov 23 15:29 ./repl/src/main/scala/ammonite/repl/interp/Interpreter.scala
haoyi-Ammonite@ ls.rec! wd/'repl/'src | (x => x.size -> x.last) sortBy (-_._1) take 3 res0: Seq[(Long, String)] = List((28167L, "EulerTests.scala"), (11686L, "Evaluator.scala"), (11022L, "Interpreter.scala"))
And lastly, here is how to performa recursive line count of all the Scala files in your current directory tree:
bash$ find ./ops/src/main -name '*.scala' | xargs wc -l 134 ./ops/src/main/scala/ammonite/ops/Extensions.scala 368 ./ops/src/main/scala/ammonite/ops/FileOps.scala 118 ./ops/src/main/scala/ammonite/ops/Model.scala 51 ./ops/src/main/scala/ammonite/ops/package.scala 238 ./ops/src/main/scala/ammonite/ops/Path.scala 84 ./ops/src/main/scala/ammonite/ops/Shellout.scala 993 total
haoyi-Ammonite@ ls.rec! wd/'ops/'src/'main |? (_.ext == "scala") | read.lines | (_.size) sum res0: Int = 996
For more examples of how to use Ammonite's pipes, check out the section on Extensions and Chaining
Ammonite provides a convenient way to spawn subprocesses using the %
and %%
commands:
%cmd(arg1, arg2)
: Spawn a subprocess with the command cmd
and command-line arguments arg1
, arg2
. print out any stdout or stderr, take any input from the current console, and return the exit code when all is done.%%cmd(arg1, arg2)
: Spawn a subprocess similar to using %
, but return the stdout of the subprocess as a String, and throw an exception if the exit code is non-zero.
For example, this is how you use the bash
command to run a standalone bash script in Bash and Ammonite:
bash$ bash ops/src/test/resources/scripts/echo HELLO HELLO
haoyi-Ammonite@ %bash('ops/'src/'test/'resources/'scripts/'echo, "HELLO") HELLO res0: Int = 0
Note that apart from quoting each path segment as a 'symbol
, we also need to quote "HELLO"
as a string. That makes things slightly more verbose than a traditional shell, but also makes it much clearer when arguments are literals v.s. variables.
If you are only passing a single argument, or no arguments, Scala allows you to leave off parentheses, as shown:
bash$ git branch 231 269 gh-pages * master
haoyi-Ammonite@ %git 'branch 231 269 gh-pages * master res0: Int = 0
bash$ date Mon Nov 23 16:54:55 SGT 2015
haoyi-Ammonite@ %date Mon Nov 23 16:55:03 SGT 2015 res0: Int = 0
You can use Ammonite-Ops' support for Spawning Subprocesses to call any external programs, even interactive ones like Python or SBT!
@ %python
Python 2.7.6 (default, Sep 9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> print "Hello %s%s" % ("World", "!"*3)
Hello World!!!
>>> ^D
res3: Int = 0
@ %sbt
[info] Loading global plugins from /Users/haoyi/.sbt/0.13/plugins
[info] Updating {file:/Users/haoyi/.sbt/0.13/plugins/}global-plugins...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Set current project to haoyi (in build file:/Users/haoyi/)
>
Ammonite-Shell uses Scala as its command and scripting language. Although the commands seem short and concise, you have the full power of the language available at any time. This lets you do things that are difficult or unfeasible to do when using a traditional shell like Bash.
Since Ammonite-Shell runs Scala code, you can perform math:
haoyi-Ammonite@ (1 + 2) * 3 res0: Int = 9 haoyi-Ammonite@ math.pow(4, 4) res1: Double = 256.0
Assign things to values (val
s):
haoyi-Ammonite@ val x = (1 + 2) * 3 x: Int = 9 haoyi-Ammonite@ x + x res1: Int = 18
Define re-usable functions:
haoyi-Ammonite@ def addMul(x: Int) = (x + 2) * 3 defined function addMul haoyi-Ammonite@ addMul(5) res1: Int = 21 haoyi-Ammonite@ addMul(5) + 1 res2: Int = 22 haoyi-Ammonite@ addMul(5 + 1) res3: Int = 24
Or make use of mutable var
s, conditionals or loops:
haoyi-Ammonite@ var total = 0 total: Int = 0 haoyi-Ammonite@ for(i <- 0 until 100){ if (i % 2 == 0) total += 1 } haoyi-Ammonite@ total res2: Int = 50
In Ammonite-Shell, everything is a typed value and not just a stream of bytes as is the case in Bash. That means you can assign them to variables and call methods on them just like you can in any programming language:
haoyi-Ammonite@ val files = ls! wd files: LsSeq = ".git" 'integration 'shell ".gitignore" 'ops 'sshd ".idea" 'project 'target ".travis.yml" 'readme "temp.html" 'LICENSE "readme.md" 'terminal "build.sbt" 'repl haoyi-Ammonite@ val count = files.length count: Int = 17
As is the case in Scala, you can annotate types.
haoyi-Ammonite@ val files: LsSeq = ls! wd files: LsSeq = ".git" 'integration 'shell ".gitignore" 'ops 'sshd ".idea" 'project 'target ".travis.yml" 'readme "temp.html" 'LICENSE "readme.md" 'terminal "build.sbt" 'repl haoyi-Ammonite@ val count: Int = files.length count: Int = 17
This is often not required (e.g. in the earlier example), since Scala has type inference, but it may make your code clearer. Furthermore, if you make a mistake, having types annotated will help the compiler give a more specific error message.
The fact that variables are typed means if you try to perform the wrong operation on a variable, you get an error even before the code runs:
haoyi-Ammonite@ val files = ls! wd files: LsSeq = ".git" 'integration 'shell ".gitignore" 'ops 'sshd ".idea" 'project 'target ".travis.yml" 'readme "temp.html" 'LICENSE "readme.md" 'terminal "build.sbt" 'repl haoyi-Ammonite@ ls + 123 Compilation Failed Main.scala:130: type mismatch; found : Int(123) required: String ls + 123 ^
The fact that Ammonite-Shell uses typed, structured values instead of byte streams makes a lot of things easier. For example, all the common data structures like Arrays and Maps are present:
haoyi-Ammonite@ val numbers = Array(1, 3, 6, 10) numbers: Array[Int] = Array(1, 3, 6, 10) haoyi-Ammonite@ numbers(0) res1: Int = 1 haoyi-Ammonite@ numbers(3) res2: Int = 10 haoyi-Ammonite@ numbers.sum res3: Int = 20 haoyi-Ammonite@ numbers(3) = 100 haoyi-Ammonite@ numbers.sum res5: Int = 110 haoyi-Ammonite@ val scores = Map("txt" -> 5, "scala" -> 0) scores: Map[String, Int] = Map("txt" -> 5, "scala" -> 0) haoyi-Ammonite@ scores("txt") res7: Int = 5
Naturally, these data structures are typed too! Trying to put the wrong sort of value inside of them results in compilation errors before the code gets a chance to run:
haoyi-Ammonite@ val numbers = Array(1, 3, 6, 10) numbers: Array[Int] = Array(1, 3, 6, 10) haoyi-Ammonite@ val myValue = "3" myValue: String = "3" haoyi-Ammonite@ numbers(myValue) // Doesn't work Compilation Failed Main.scala:131: type mismatch; found : String required: Int numbers(myValue) // Doesn't work ^ haoyi-Ammonite@ numbers(1) = myValue // Also doesn't work Compilation Failed Main.scala:131: type mismatch; found : String required: Int numbers(1) = myValue // Also doesn't work ^ haoyi-Ammonite@ // Need to convert the string to an Int haoyi-Ammonite@ numbers(myValue.toInt) res2: Int = 10 haoyi-Ammonite@ numbers(1) = myValue.toInt haoyi-Ammonite@ numbers(1) = "2".toIntIn general, apart from the filesystem-specific commands, you should be able to do anything you would expect to be able to do in a Scala shell or Java project. This documentation isn't intended to be a full tutorial on the Scala language, check out the Scala Documentation if you want to learn more!
Apart from the pipe operators described in the earlier section on Piping, Ammonite-Shell allows you to call any valid Scala method on any value; it's just Scala after all! Here's an example using normal Scala collection operations to deal with a list of files, counting how many files exist for each extension:
haoyi-Ammonite@ val allFiles = ls.rec! 'ops/'src/'test/'resources allFiles: LsSeq = 'scripts 'testdata 'scripts/'echo 'scripts/'echo_with_wd 'testdata/"File.txt" 'testdata/'folder1 'testdata/'folder2 'testdata/'folder1/"Yoghurt Curds Cream Cheese.txt" 'testdata/'folder2/'folder2a 'testdata/'folder2/'folder2b 'testdata/'folder2/'folder2a/"I am.txt" 'testdata/'folder2/'folder2b/"b.txt" haoyi-Ammonite@ val extensionCounts = allFiles.groupBy(_.ext).mapValues(_.length) extensionCounts: Map[String, Int] = Map("txt" -> 4, "" -> 8)
Any Java APIs are likewise available:
haoyi-Ammonite@ System.out.println("Hello from Java!") Hello from Java! haoyi-Ammonite@ import java.util._ import java.util._ haoyi-Ammonite@ val date = new Date() date: Date = Mon Nov 23 16:56:53 SGT 2015 haoyi-Ammonite@ date.getDay() res3: Int = 1
In fact, Ammonite-Shell allows you to ask for any published third-party Java/Scala library for usage in the shell, and have them downloaded, automatically cached, and made available for use. e.g. we can load popular libraries like Google Guava and using it in the shell:
haoyi-Ammonite@ import com.google.common.collect.ImmutableBiMap // Doesn't work Compilation Failed Main.scala:128: object google is not a member of package com import com.google.common.collect.ImmutableBiMap // Doesn't work ^ haoyi-Ammonite@ load.ivy("com.google.guava" % "guava" % "18.0") // Load from Maven Central :: loading settings :: url = jar:file:/Users/haoyi/Dropbox%20(Personal)/Workspace/Ammonite/repl/target/scala-2.11/ammonite-repl-0.5.0-2.11.7!/org/apache/ivy/core/settings/ivysettings.xml :: resolving dependencies :: com.google.guava#guava-caller;working confs: [default] found com.google.guava#guava;18.0 in central found com.google.code.findbugs#jsr305;1.3.9 in chain-resolver :: problems summary :: :::: ERRORS unknown resolver null :: USE VERBOSE OR DEBUG MESSAGE LEVEL FOR MORE DETAILS haoyi-Ammonite@ import com.google.common.collect.ImmutableBiMap // Works now import com.google.common.collect.ImmutableBiMap // Works now haoyi-Ammonite@ val bimap = ImmutableBiMap.of(1, "one", 2, "two", 3, "three") bimap: ImmutableBiMap[Int, String] = {1=one, 2=two, 3=three} haoyi-Ammonite@ bimap.get(1) res3: String = "one" haoyi-Ammonite@ bimap.inverse.get("two") res4: Int = 2
Or Joda Time:
haoyi-Ammonite@ load.ivy("joda-time" % "joda-time" % "2.8.2") :: loading settings :: url = jar:file:/Users/haoyi/Dropbox%20(Personal)/Workspace/Ammonite/repl/target/scala-2.11/ammonite-repl-0.5.0-2.11.7!/org/apache/ivy/core/settings/ivysettings.xml :: resolving dependencies :: joda-time#joda-time-caller;working confs: [default] found joda-time#joda-time;2.8.2 in central found org.joda#joda-convert;1.2 in central haoyi-Ammonite@ import org.joda.time.{DateTime, Period, Duration} import org.joda.time.{DateTime, Period, Duration} haoyi-Ammonite@ val dt = new DateTime(2005, 3, 26, 12, 0, 0, 0) dt: DateTime = 2005-03-26T12:00:00.000+08:00 haoyi-Ammonite@ val plusPeriod = dt.plus(Period.days(1)) plusPeriod: DateTime = 2005-03-27T12:00:00.000+08:00 haoyi-Ammonite@ dt.plus(new Duration(24L*60L*60L*1000L)) res4: DateTime = 2005-03-27T12:00:00.000+08:00
See the section on Artifact Loading to learn more.
You can write scripts in the same way you write commants, and load them using the load.script(...)
and load.module(...)
methods. To read more about this, check out the documentation on Script Files.
Ammonite-Shell takes a fundamentally different architecture from traditional shells, or even more-modern shell-alternatives. Significant differences include:
In this section we'll examine each of these decisions and their consequences in turn. As the incumbents in this space, we'll be looking at traditional system shells like Bash, Zsh or Fish, as well as popular non-system REPLs like the Python/IPython REPL.
The use of Scala as the command & scripting language is unusual among shells, for many reasons. Firstly, most shells implement their own, purpose built language: Bash, Zsh, Fish, and even more obscure ones like Xonsh each implement their own language. Secondly, all of these languages are extremely dynamic, and apart from those most popular languages with REPLs (Python, Ruby, Javascript, ...) tend to be dynamical, interpreted languages. Scala falls at the opposite end of the spectrum: statically typed and compiled.
Scala brings many changes over using traditional dynamic, interpreted REPL languages:
Apart from the differences between Scala and dynamic languages (Python, Ruby, etc.) for REPL usage, Scala is even further away from the sort of ad-hoc, ultra-dynamic languages most often associated with traditional shells (Bash, sh, zsh, etc.). In particular:
String
s, proper numbers like Int
or Double
, absolute Path
s and relative RelPath
s, Array
s, Map
s, Iterator
s and all sorts of other handy data-structures. Many commands return objects which have fields: this sounds simple until you realize that none of bash/zsh/fish behave this way. %
syntax, most commands like ls!
and rm!
are simple functions living in-process rather than in separate processes. This reduces the overhead as compared to spawning new processes each time, but does cause some additional risk: if a command causes the process to crash hard, the entire shell fails. In bash, only that command would fail.The latter set of tradeoffs would be also present in many of the shell-replacements written in dynamic languages, like Xonsh which is written in Python. The earlier set, on the other hand, are pretty unique to Ammonite using Scala. There are both positive and negative points in this list.
Running Ammonite directly on the JVM again is very different from how most shells work: most have their own scripting language, and their own interpreter. Most are implemented in C. What is it like running your code directly as bytecode on the JVM? Here are some of the negatives:
.jar
file. That's already larger than most other shells out there, and gets >100mb larger if you bundle the JVM along with it! In general, the JVM class-file format is bloated and inefficient, and there is no way to exclude to numerous un-needed parts of the JVM during the initial download. Project Jigsaw will help with this when it finally lands in Java 9.In general, the JVM has traditionally been used as a server-side platform for long-runing services, and its slow-startup and bloated disk/memory footprints are a symptom of that. Running on the JVM also has some upsides, though:
load.ivy
them from Ammonite, and Java's excellent dependency-management infrastructure will download them (along with any transitive dependencies!), cache them locally, and make them available to your code.
There are both pros and cons with running Ammonite on the JVM: we gain its heavy startup/memory overhead, but also get access to its high-performance JIT, massive ecosystem of available packages.
Overall, Ammonite-Shell blurs the line between a "programming language REPL" like IPython or Ruby's IRB and a "system shell" like Bash or Zsh. Like system shells, Ammonite-Shell provides concise filesystem operations, path-completion, and easy spawning of subprocesses. Like programming language REPLs, it provides a full-fledged, general-purpose language for you to use, rather than a crippled cut-down command-language that is available in most system shells.
The goal is to provide something general enough to use as both a system shell and a general-purpose programming language. Traditionally, there has always been some tension when deciding between these:
Traditionally, there really has been no good answer to this dilemma: whether you use Bash or Python to write your scripts, whether you use Bash or Python as your shell, there is always something frustrating about the set-up.
With Ammonite-Shell, there is no dilemma. You can use the same concise, general-purpose language for your shell as you would for your scripts, large or small. In Ammonite-Shell, you can concisely deal with files at the command-line with the same language you use to write maintainable scripts, large or small, and the same language that you use to write rock-solid application code.
Ammonite is primarily maintained by Li Haoyi, with a lot of help from Laszlo Mero over the summer through Google Summer of Code, and help from many other contributors. We have an active Gitter channel and a mailing list:
I've also given a number of talks about Ammonite:
def<tab>
auto-complete crasher #257, thanks to Matthew Edwards![Enter]
now only submits the input if your cursor is at the bottomSeq[String]
s into subprocess argumentscd!
, wd
, path-completion) has been pulled out of Ammonite-REPL, and is now available separately as Ammonite-Shell.ls
and ls.rec
commandsLoad.ivy
now properly attempts to load artifacts from the local ~/.ivy/cache
, ~/.ivy/local
and ~/.m2
folders, before fetching from maven centralcache1234567890abcdef1234567890abcdef
objects from the autocomplete list, because they're not helpfulAny
from the default import lists.read.lines
and ls
/ls.rec
@
-delimited block in a script loaded via load.module
gets its names dumped into the REPL's environment now, letting you create some semblance of hygiene, thanks to Laszlo MeropathSeparator
so Ammonite-REPL is at least basically-runnable on windows, although buggytoString
~/.ammonite
folder. This includes ~/.ammonite/history
, ~/.ammonite/predef.scala
, and various cache, thanks to Laszlo Mero~/.ammonite/predef.scala
Configuration file which will be executed the first thing when the Ammonite REPL is loaded. This is useful for common imports, load.ivy
ing libraries, or other configuration for your REPLload.exec
and load.module
, thanks to Laszlo MeroREPL
s constructor is now done in-REPL,load.ivy
no longer causes all existing values to be lazily recomputed.Unit
are no longer echo-ed to the userload.ivy
is now cached after the first call, letting you e.g. load libraries in your ~/.ammonite/predef.scala
without waiting for the slow ivy-resolution every startupgit
commands, edit files via vim
, open ssh
sessions or even start SBT
or Python
shells right from your Scala REPLPPrint
s are much lazier and will avoid stringifying the whole collection if its going to get truncated anyway.TPrint[T]
s to provide custom printing for any type.predef
parameter to be passed into Repl.run()
call, letting you configure initialization commands or importsCtrl-C
and Ctrl-D
handling, to make it behave more like other REPLs