Hinweis: Dieser Beitrag ist nur auf Englisch verfügbar.
We often extend Content Management Systems for our customers. Predictably, this means that we need to interact with CMS services allowing remote access to content and data. Tasks for which this interaction is necessary are ofter either repetitive - like uploading development content - or formulaic - like migrating content to a new format. Naturally, this makes them prime candidates for automation.
As CMS services often have Java bindings available and we're a Scala company, we've begun considering using Scala to solve the issue. After all, Scala is both known for its strong static typing - which lends itself well to ensuring that, for instance, client's content is migrated safely - and its remarkable terseness which sometimes makes people confuse it with a dynamic language. This is where Ammonite, a modernized Scala REPL and script runner, comes in. Ammonite can run Scala files as though they were scripts. "Hello, world" is as simple as:
#!/usr/bin/env amm
println("Hello, world!")
Notice that there's zero fuss necessary - we just write the code that we want to be executed with no ceremony. Of course, this is not where Ammonite's usefulness ends - the nice thing about it is that it not only makes writing Scala scripts possible, it also makes it easy. Let's see a script that will output the status of a specific issue on GitHub:
#!/usr/bin/env amm
import $ivy.`org.kohsuke:github-api:1.93`
import org.kohsuke.github._
@main def main(repo: String, issue: Int) = {
val gh = GitHub.connectAnonymously()
val issueState =
gh.getRepository(repo).getIssue(issue).getState
println(s"$repo issue #$issue is $issueState")
}
Example interaction with this script looks like this:
> ./check-issue.sh --repo lihaoyi/ammonite --issue 10
lihaoyi/ammonite issue #10 is CLOSED
This time, we did need to write a main
method - but this was because we needed to parse arguments. Notice that there's no argument parsing code in the script - we simply annotated a method with @main
and Ammonite took care of the rest. We also needed to include a dependency, which again was done with an absolute minimum of fuss - all that was necessary was a "magic" $ivy
import. In Python, the library would need to be downloaded before the script can be run, but Ammonite takes care of that on its own - if the library is not available locally, it will be downloaded when the script is executed for the first time.
Ammonite also allows scripts to include other scripts:
// banner.sc
def display(text: String): Unit = {
println("*" * (text.length + 5))
println(s"* $text *")
println("*" * (text.length + 5))
}
// greet.sc
#!/usr/bin/env amm
import $file.banner
println("Hello! What is your name?")
val name = Console.readLine()
banner.display(s"Hello, $name!")
The above greet.sc
behaves like this:
> ./greet.sc
Hello! What is your name?
Alex
****************
* Hello, Alex! *
****************
This feature is very much necessary when scripting remote services, as we definitely do not want to duplicate service-related code inside each script. More interestingly, it plays very nicely with @main
methods - a script can call other scripts in a simple way and remain type-safe since, after all, the entrypoints to other scripts are just methods.
Last but not least, Ammonite also helps with developing scripts. Since Scala is statically typed, we can expect a type error or ten when writing our scripts. Manually re-running them can get tedious, so Ammonite can instead watch script files and re-run them on each change if --watch
flag is set. If combined with --predef
flag, a REPL will instead be opened with the script's top-level definitions available. Ammonite allows opening source code for methods and objects and additionally has great tab-completion and multi-line editing - all of these features allow using Ammonite as a quasi-IDE for progressively writing the script.
An Example
To illustrate the points so far, we've prepared an example repository that allows interacting with a Northwind-like database.
> git clone $REPO && cd $REPONAME
> # we will now bring up a Docker container with a DB and insert data into it
> cmd/setup.sc
Docker container is up and running
Inserted:
10 employees
92 customers
101 orders
> # Check the last order in the database
> cmd/order.sc last
Last order: Order(10348,5,FAMIA,Familia Arquibaldo,Sao Paulo)
> # Add a new one and see if it was inserted
> cmd/order.sc place --employee 2 --customer ANTON --shipName Boat --shipCity London
> cmd/order.sc compare
Last local order (10348) is 2 order(s) behind
> cmd/order.sc last
Last order: Order(10349,2,ANTON,Boat,London)
> # Finally, check the status of the Docker container and bring it down
> cmd/docker.sc status
Docker container is up and running
> cmd/docker.sc down
Scripts inside the cmd
directory can be used to set up and tear down the local environment, download remote data and update it. Some of the scripts are lower-level than others - for example, docker.sc
is responsible for Docker and is run by setup.sc
when setting up the environment. If necessary, cmd/repl
can be used to open up a REPL with all the scripts loaded and ready to run. This is very helpful if you're not entirely familiar with the scripts since inside the REPL we have tab-completion available. Since argument parsing is based on methods, running the scripts from the REPL is very similar to running them normally:
> cmd/repl
Welcome to the Ammonite Repl 1.1.2
(Scala 2.12.6 Java 1.8.0_144)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ order.place(
employee = 2, customer = "ANTON", shipName = "Boat", shipCity = "London"
)
@ order.compare()
Last local order (10347) is 1 order(s) behind
@ order.last()
Last order: Order(10348,2,ANTON,Boat,London)
On a higher level, it's worth noticing that there are two kinds of Scala files in the repository - "scripts", which are directly in cmd
directory, and "modules" contained inside cmd/modules
. "Modules" are code common to most, if not all scripts - in this specific case they are essentially bindings for the Northwind database. When interacting with a service for which only Java bindings are available, modules would mostly contain Scala adapters instead. A very good reason for organizing code like this, besides avoiding code duplication, is that it's very easy to create a library out of such modules - each file can be directly converted to an object. This, in turn, is very handy if it turns out that another project needs similar scripts - common parts can simply be lifted to a library, ready to be downloaded from the company repository.
This brings us to the last, but not least, problem which our example repository demonstrates how to solve - custom repositories and authorization. While Ammonite by default has support for the former, it doesn't really support the latter. We've prepared a minimal wrapper around Ammonite which solves this problem, aptly named amm+auth
. bin/try-auth.sh
will start a Bash session where amm
is based on amm+auth
:
> bin/try-auth.sc
bash-4.4$ bin/is-prime.sc 5
true
bash-4.4$ cat bin/is-prime.sc
#!/usr/bin/env amm
import $ivy.`org.apache.commons:commons-math3:3.4.1.redhat-3`
@main def check(n: Int) =
org.apache.commons.math3.primes.Primes.isPrime(n)
bash-4.4$ ^D
As we can see, is-prime.sc
uses a specific version of Apache commons-math3
, only available from the Red Hat Maven repository. You can try running it without using try-auth.sh
- it won't work! bin/try-auth.sc
adds bin
to your $PATH
, which contains amm
script using amm+auth
internally. Hypothetically, if the Red Hat required authorization for its Maven repository, storing the credentials in your home directory so amm+auth
can use them would be as simple as:
bash-4.4$ coursier+auth cred set redhat
user: redhat-user
password:
If you're interested in seeing how amm+auth
can help you, head over to its repo at https://github.com/BrightIT/coursier-plus-auth.
Conclusion
To sum up: we've found the approach we've presented so far very useful for developing scripts that can interact with remote services. Ammonite not only allows writing such scripts, but it also lends itself naturally to building simple command-line interfaces out of them and, if necessary, creating libraries out of CLIs developed this way. Additionally, Ammonite's features are helpful enough during development that often no IDE is necessary. If you're too often manually dealing with services for which Java bindings are available, then scripting those interactions with Ammonite might be just what you need.