-
-
Notifications
You must be signed in to change notification settings - Fork 372
/
Scripts.scalatex
753 lines (606 loc) · 28.3 KB
/
Scripts.scalatex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
@import Main._
@import readme.Sample._
@import ammonite.ops._
@val ammoniteTests = 'amm/'repl/'src/'test/'scala/'ammonite/'session
@val replSource = 'amm/'src/'main/'scala/'ammonite
@val advancedTests = ammoniteTests/"AdvancedTests.scala"
@val scriptTests = 'amm/'src/'test/'scala/'ammonite/'session/"ScriptTests.scala"
@sect("Scala Scripts", "Lightweight Programming in Scala")
@p
@b{Scala scripts} are lightweight files containing Scala code that
can be directly run from the command line. Unlike normal Scala projects,
Scala scripts let you save and run code without setting up a "build-file"
or "project". Scala Scripts are useful for writing small pieces of code,
and are much quicker to write and deploy than a full-fledged SBT project.
@img(src:="Watch.gif", display.block, marginLeft.auto, marginRight.auto, height := 322, loadingLazy)
@p
Creating an Ammonite Script is just a matter of creating a
@code{MyScript.sc} with some Scala code in it, and running it
@sect.ref("Running Scripts", "from your terminal"). Deploying the script is
a matter of copying the script file to wherever you want to run it, and
running it. No @code{project/} folder, no worrying about @code{.jar} files
or uber-jars. No worrying about compiling your code: scripts are
automatically compiled the first time they are run, and subsequently start
quickly with minimal overhead. Writing and running Scala code
doesn't get much easier than that!
@p
As an example, Ammonite's own
@lnk("Continuous Integration Scripts", "https://github.com/lihaoyi/Ammonite/tree/main/ci")
are written as @code{.sc} Scala Scripts, as are @lnk("Haoyi's blog",
"https://github.com/lihaoyi/blog") and @lnk("resume",
"https://github.com/lihaoyi/Resume"). These are all examples of using
Scala Scripts to do some simple (or not so simple!) tasks in just a few
files, without the hassle of setting up a heavyweight SBT project.
@p
To begin with, download the Ammonite @ammonite.Constants.version
script-runner for Scala 2.13 (also available for
@sect.ref{Older Scala Versions}):
@hl.sh
@replCurl
@p
Or to try out the @sect.ref("Unstable Changelog", "latest features") in our
@sect.ref("Unstable Versions", "Unstable Release")
@ammonite.Constants.unstableVersion:
@hl.sh
@readme.Sample.unstableCurl
@p
And read on to learn about how Scala scripts work.
@sect{Script Files}
@p
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.
@hl.scala
// MyScript.sc
// print banner
println("Hello World!!")
// common imports
import sys.process._
import collection.mutable
// common initialization code
val x = 123
println("x is " + 123)
...
@p
You can write any Scala code you want in an Ammonite script, including
top-level statements and definitions (e.g. the @code{println} and
@hl.scala{val x = 123} above) that are not valid in "normal" Scala
projects. You do not need to wrap these sorts of top-level statements
or expressions in boilerplate @hl.scala("object Foo{...}") wrappers:
this is all done automatically for you by Ammonite.
@p
After that, it's a matter of running the script
@sect.ref{From the REPL} or @sect.ref{From Bash}, e.g.
@hl.sh
bash$ amm MyScript.sc
@hl.sh
Hello World!!
x is 123
...
@sect{Script Imports}
@p
No code stands alone; scripts depend on other scripts. Often they depend
on third party libraries, as there's so much code out there already
written it doesn't make sense to reinvent everything yourself.
@p
Ammonite Scripts allow you to import @sect.ref{Other Scripts}, just like
any Bash or Python scripts do. Furthermore, they let you cleanly depend
on third party libraries: since Ammonite runs on the @lnk("JVM",
"https://en.wikipedia.org/wiki/Java_virtual_machine"), this means
@sect.ref{Ivy Dependencies}. Ammonite will ensure that the relevant
dependencies are always downloaded and used, and you never need to worry
about remembering to "install" things before running your scripts!
@sect{Other Scripts}
@p
Like other scripting languages, Ammonite Scripts allow you to break your
script into multiple files, and import them from each other in order to
use what is in each file. Unlike "Normal" Scala projects, there is no
need to set up a @code{src/main/scala} folder, and create a build file,
and all these other things: simply split your script into two files, and
import one from the other using @sect.ref{import $file}:
@hl.ref('amm/'src/'test/'resources/'importHooks/"Basic.sc")
@hl.ref('amm/'src/'test/'resources/'importHooks/"FileImport.sc")
@p
Here, we are defining a simple @hl.scala{val} in @code{Basic.sc}, and
then importing it from @code{FileImport.sc}. And of course, we can
use what we defined in @code{FileImport.sc} and import it in another
file
@hl.ref('amm/'src/'test/'resources/'importHooks/"IndirectFileImport.sc")
@p
And so on, importing files as many or as deep as you want. You can
use @hl.scala{^} segments at the start of your @hl.scala{import $file}
to import things from outside the current script's enclosing folder, e.g.
@hl.scala{import $file.^.^.foo} will import the script file
@code{../../foo.sc} and make it available for you to use.
@p
@hl.scala{$file} imports inside Scala Scripts behave the same as
@hl.scala{$file} imports @sect.ref("import $file",
"within the Ammonite-REPL"), and have the same characteristics:
@ul
@li
@sect.ref{Imported Scripts are Re-used}
@li
@sect.ref{Cannot directly import from inside a Script}
@li
@sect.ref{Renamed-scripts and multiple-scripts}
@sect{Ivy Dependencies}
@p
You can easily make use of external Ivy/Maven artifacts right in your
scripts, without needing to set up a separate build file. Simply use a
@sect.ref{import $ivy}, just as you would in the
@sect.ref{Ammonite-REPL}, and it will be available in the script for you
to use, e.g. here we make use of the @lnk("Scalatags",
"http://www.lihaoyi.com/scalatags/") library:
@hl.ref('amm/'src/'test/'resources/'importHooks/"IvyImport.sc")
@p
If you need more detailed control over what you are importing, e.g.
with attributes, classifiers or exclusions, you can fall back to
using the @hl.scala{interp.load.ivy(deps: coursierapi.Dependency*)}
function.
@hl.ref(scriptTests, Seq("loadIvyAdvanced", "@"), "\"\"\"")
@p
Note that to use this function, your script needs to be a
multi-stage script as listed below, and the @hl.scala{interp.load.ivy}
call needs to be in an earlier block
@sect{Multi-stage Scripts}
@p
By default, everything in a script is compiled and executed as a single
block. While you can use @sect.ref{Magic Imports} to load other scripts
or Ivy/Maven artifacts before your script runs, those can only load "hardcoded"
scripts or artifacts, and cannot e.g. load different scripts depending on
some runtime variables.
@p
If you want to load different scripts or Ivy/Maven artifacts depending on
runtime values, you can use the runtime-equivalent of magic imports:
@ul
@li
@hl.scala{import $cp} becomes @hl.scala{interp.load.cp}
@li
@hl.scala{import $file} becomes @hl.scala{interp.load.module}
@li
@hl.scala{import $ivy} becomes @hl.scala{interp.load.ivy}
@p
These are plain-old-Scala-functions that let you pass in a
@hl.scala{Path} to a script to load, or load different Ivy/Maven artifacts
depending on runtime values. Additionally, there is an overloaded
version of @hl.scala{interp.load.cp} which takes a @hl.scala{Seq[Path]}
of classpath entries. This variant is much more efficient for adding
multiple classpath entries at once.
@p
Since these functions get run *after* the current compilation block is
compiled, you need to split your script into two compilation blocks,
and can only use the results of the @hl.scala{load}ed code in
subsequent blocks:
@hl.scala
// print banner
println("Welcome to the XYZ custom REPL!!")
val scalazVersion = "7.2.27"
interp.load.ivy("org.scalaz" %% "scalaz-core" % scalazVersion)
// This @@ is necessary for Ammonite to process the `interp.load.ivy`
// before continuing
@@
// common imports
import scalaz._
import Scalaz._
// use Scalaz!
...
@p
In general, this should not be necessary very often: usually you should
be able to load what you want using @sect.ref{Magic Imports}.
Nevertheless, sometimes you may find yourself needing to get "under the
hood" and use these @hl.scala{load}ing functions directly. When that
happens, using @sect.ref{Multi-stage Scripts} is the way to go.
@sect{Script Arguments}
@p
Often when calling a script from the external command-line (e.g.
Bash), you need to pass arguments to configure its behavior. With
Ammonite, this is done by defining a @hl.scala("@main") method, e.g.
@hl.ref(wd/'amm/'src/'test/'resources/'mains/"Args.sc")
@p
When the script is run from the command line:
@hl.sh
$ amm Args.sc 3 Moo
"Hello! MooMooMoo Ammonite."
@p
The top-level definitions execute first (e.g. setting @hl.scala{x}),
and then the @hl.scala("@main") method is called with the arguments you
passed in. Note that the return-value of the script is pretty-printed
by default, which quotes strings and may nicely format/indent lists or
other data-structures. If you want to avoid this default pretty-printing
behavior, annotate your @hl.scala("@main") method as returning
@hl.scala{: Unit} and add your own @hl.scala{println}s:
@hl.ref(wd/'amm/'src/'test/'resources/'mains/"Args2.sc")
@hl.sh
$ amm Args2.sc 3 Moo
Hello! MooMooMoo Ammonite
@p
You can also pass in named arguments using @code{--} to demarcate
them:
@hl.sh
$ amm Args.sc -i 3 -s Moo
"Hello! MooMooMoo Ammonite."
@p
Default arguments behave as you would expect (i.e. they allow you to
omit it when calling). Arguments are parsed using the
@hl.scala{mainargs.Parser*} parsers, which provides parsers for primitives
like @code{Int}, @code{Double}, @code{String}, as well as basic
data-structures like @code{Seq}s (taken as a comma-separated list) and
common types like Paths.
@p
If you pass in the wrong number of arguments, or if an argument fails
to deserialize, the script will fail with an error message.
@p
The @hl.scala{main} method does not get automatically called when you
@hl.scala{load.module} or @hl.scala{load.exec} a script from @i{within}
the Ammonite REPL. It gets imported into scope like any other method or
value defined in the script, and you can just call it normally.
@p
@hl.scala{vararg*} arguments work as you would expect as well, allowing one or
more arguments to be passed from the command-line and aggregated into
a @code{Seq} for your function to use:
@hl.scala
@@main
def entrypoint(args: String*) = {
...
}
@p
in which case Ammonite will take all arguments and forward them to your
main method unchecked and unvalidated, from which point you can deal
with the raw @code{Seq[String]} however you wish. Note that
@hl.scala{vararg*} arguments cannot be passed by-name, e.g. via
@code{--args foo}
@sect{Ammonite Arguments in Scripts}
@p
Ammonite passing any arguments that come @i{before} the script file to
Ammonite itself, while arguments that come @i{after} the script file are
given to the script:
@hl.sh
$ amm --predef-code 'println("welcome!")' Args.sc 3 Moo
welcome!
"Hello! MooMooMoo Ammonite."
@p
Note that on Windows you have to escape the quotes differently:
@hl.sh
C:\> amm --predef-code "println(\"welcome!\")" Args.sc 3 Moo
@p
Here, "Ammonite Arguments" go on the @i{left} of the @code{Args.sc}, while
@i{Script Arguments} go on the @i{right} of the @code{Args.sc}. The script
arguments on the right can also be empty if you don't want to pass
any arguments to the script.
@p
If you want to define a script with a Shebang line that runs Ammonite
with particular arguments, you can use
@hl.sh
#!/bin/bash
exec amm --predef 'println("welcome!")' "$0" "$@@"
!#
@p
And which will pass in the @code{--predef} flag to Ammonite while
running the script via @code{./Args.sc}. If you want to then pass
in different sets of arguments, you can run the script using @code{amm}
e.g. @hl.sh{amm --predef 'println("Hello!")' Args.sc 3 Moo} as before.
(Note that while a single-line @code{#!/usr/bin/env amm --predef '...'}
shebang may work on some systems such as OS-X, it is not portable and
would not work on Linux)
@sect{Multiple Main Methods}
@p
If you have only a single @hl.scala("@main") method, any arguments that
you pass to the script get used as arguments to that @hl.scala("@main").
But if you have @i{multiple} @hl.scala("@main") methods, the first
argument to the script is used to select which @hl.scala("@main") method
to call. e.g. given:
@hl.ref(
wd/'amm/'src/'test/'resources/'mains/"MultiMain.sc",
className = "scala"
)
@p
You can call it via
@code
amm MultiMain.sc mainA
@p
Or
@code
amm MultiMain.sc functionB 2 "Hello"
@sect{Script Usage Docs}
@p
You can document your scripts with the @hl.scala("@arg") annotation. By
default, a script such as
@hl.ref(
wd/'amm/'src/'test/'resources/'mains/"MultiMain.sc",
className = "scala"
)
@p
Will result in a usage message:
@hl.ref(
wd/'amm/'src/'test/'scala/'ammonite/'main/"MainTests.scala",
start = Seq("specifyMain", "\"\"\"", ""),
end = "\"\"\"",
className = "sh"
)
@p
You can add docs via
@hl.ref(
wd/'amm/'src/'test/'resources/'mains/"MultiMainDoc.sc",
className = "scala"
)
@p
Which will be shown as part of the usage message
@hl.ref(
wd/'amm/'src/'test/'scala/'ammonite/'main/"MainTests.scala",
start = Seq("specifyMainDoc", "\"\"\"", ""),
end = "\"\"\"",
className = "sh"
)
@sect{Bundled Libraries}
@p
While Ammonite allows you to load any Java or Scala library for use
via the @sect.ref{import $ivy} syntax, it also comes bundled with some
basic libraries, e.g. @lnk("Requests-scala", "https://github.com/lihaoyi/requests-scala")
for making HTTP calls, or the
@lnk("uPickle", "https://github.com/lihaoyi/upickle")
library with its @lnk("JSON Api", "http://www.lihaoyi.com/upickle/#uJson")
for dealing with the common JSON format.
@p
For example, here's a tiny script that offers two main methods, one
to shorten a github link using Requests-Scala and the @code{git.io} API,
and one that pulls out a list of release-names from a given github
project using Requests-Scala, uPickle's JSON package, and the Github API:
@hl.ref(wd/"integration"/"src"/"test"/"resources"/"ammonite"/"integration"/"basic"/"HttpApi.sc")
@p
You can run @code{amm} on the script to see what it can do
@hl.sh
> amm HttpApi.sc
Need to specify a sub command: addPost, comments
@p
And you can run the two functions (after using @code{chmod +x} to make
the file executable) via
@hl.sh
> ./HttpApi.sc addPost MyTitle MyContent
@p
Or
@hl.sh
> ./HttpApi.sc comments 100
"et occaecati asperiores quas voluptas ipsam nostrum,doloribus dolores ut dolores occaecati,dolores minus aut libero,excepturi sunt cum a et rerum quo voluptatibus quia,ex eaque eum natus"
@sect{Script Builtins}
@p
Apart from bundling some third-party libraries for convenience,
Ammonite also provides some builtins you can use from scripts to
inspect and manipulate the interpreter itself. Note that this is
a much smaller set of functionality than the set of
@sect.ref{Builtins} available to the REPL: it won't have things
like the @code{repl.prompt}, @code{repl.history}, and other things
that only really make sense in the interactive REPL.
@hl.ref('amm/"interp"/"api"/'src/'main/'scala/'ammonite/'interp/'api/"InterpAPI.scala", "trait InterpAPI")
@sect{Script Predef}
@p
If you want code to be loaded before you run any script, you can place
it in @code{~/.ammonite/predefScript.sc}. This is distinct from the REPL
pre-defined code which lives in @code{~/.ammonite/predef.sc}.
@sect{Running Scripts}
@p
There are two way main ways to run Ammonite scripts:
@sect.ref{From the REPL} and @sect.ref{From Bash}.
@sect{From Bash}
@p
Apart from loading scripts within the @sect.ref{Ammonite-REPL}, You can
also run scripts using the Ammonite executable from an external shell
(e.g. bash):
@hl.sh
@replCurl
@hl.scala
$ amm path/to/script.sc
@p
All types, values and imports defined in scripts are available to
commands entered in REPL after loading the script.
@p
You can also make an Ammonite script self-executable by using a shebang
@hl.scala{#!}. This is an example script named @hl.scala{hello}. There
is no need to add the @hl.scala{.sc} extension. The @hl.scala{amm}
command needs to be in the @hl.scala{PATH}:
@hl.sh
#!/usr/bin/env amm
println("hello world")
@p
make it executable and run it from an external shell (e.g. bash):
@hl.sh
$ chmod +x /path/to/script
$ /path/to/script
@p
Ammonite also supports the @code{JAVA_OPTS} environment variable for
passing arguments to the JVM that it runs inside, e.g. you can pass in
a custom memory limit via
@hl.scala
bash$ JAVA_OPTS="-Xmx1024m" amm path/to/script.sc
@p
To let it use only up to 1024 megabytes of memory
@sect{Watch and Reload}
@img(src:="Watch.gif", display.block, marginLeft.auto, marginRight.auto, height := 322, loadingLazy)
@p
Ammonite provides the @code{-w}/@code{--watch} flag, which tells it to
not exit when a script completes, but instead watch the files that were
run, and re-run them when any of them change. You can use this flag via
@code
$ amm -w foo.sc
@p
Within your scripts, you can also flag other files you want Ammonite
to watch, via the @code{interp.watch(p: Path)} function. This is useful
if you are iterating on a script together with some external data files
the script depends on, and you want to
@sect{Script Debug REPL}
@img(src:="ScriptRepl.gif", display.block, marginLeft.auto, marginRight.auto, height := 432, loadingLazy)
@p
When a script is not working as intended, it is useful to be able to
poke around in a REPL after the script has run, in order to see what
values are stored in which variables or what methods are available via
autocomplete. To do so, you can run the script using the
@code{--predef}/@code{-p} flag.
@code
$ amm --predef foo.sc
@p
This will run the script as normal, but on completion open up a REPL
which has all the values defined in that script imported and ready to
use. You can then poke around within the REPL as you wish.
@p
Using @code{--predef}/@code{-p} to run a script and then open an
interactive REPL can be combined with the @code{--watch}/@code{-w}
flag:
@code
$ amm --watch --predef foo.sc
@p
This will open up a REPL after the script runs, and when you exit the
REPL it will watch the script file and the files that the script depends
on, re-running the script and REPL if any of them change.
@p
@code{--predef}/@code{-p} can be used to include a script file as a
predef before running any script or REPL, which is useful for a range
of things apart from serving as a debug REPL on any script.
@sect{From the REPL}
@p
You can load any script into the Ammonite REPL using the
@hl.scala{import $file} syntax, for example here we import the above
@code{MyScript.sc} file to access its @hl.scala{x} value:
@hl.scala
@@ x // doesn't work yet
Compilation Failed
cmd0.sc:1: not found: value x
val res0 = x // doesn't work yet
^
@@ import $file.MyScript
Welcome to the XYZ custom REPL!!
@@ MyScript.x // You can refer to definitions from that module
res1: Int = 123
@@ import MyScript._
@@ x // works
res2: Int = 123
@p
You can also import the module, and any associated definitions you want,
in the same import:
@hl.scala
@@ x // doesn't work yet
Compilation Failed
cmd0.sc:1: not found: value x
val res0 = x // doesn't work yet
^
@@ import $file.MyScript, MyScript._
Welcome to the XYZ custom REPL!!
@@ x
res1: Int = 123
@p
Note that by default, scripts imported via @code{$file} are
@i{encapsulated}, so any imports inside that @code{MyScript} performs are
not available outside of @code{MyScript.sc}:
@hl.scala
@@ import $file.MyScript, MyScript._
Welcome to the XYZ custom REPL!!
import $file.$
@@ mutable.Buffer(x)
cmd1.sc:1: not found: value mutable
val res1 = mutable.Buffer(x)
^
Compilation Failed
@p
As you can see, even though @code{collection.mutable} was imported inside
@code{MyScript.sc}, you cannot use them outside after importing it.
@p
If you want to make everything (both imports and definitions) available
by default after importing, you can use an @code{$exec} import instead of
@code{$file}:
@hl.scala
@@ import $exec.MyScript
Welcome to the XYZ custom REPL!!
import $exec.$
@@ mutable.Buffer(x)
res1: mutable.Buffer[Int] = ArrayBuffer(123)
@p
As you can see, now @code{mutable} is available, and so is @code{x} even
though we did not directly import it.
@p
While @code{$file} imports are useful for defining re-usable modules with
common functions and definitions, @code{$exec} imports are useful as
aliases for common setup to get your REPL environment just the way you
want it. Of course, any files you import via @hl.scala{import $file} or
@hl.scala{import $exec} can themselves import other Scala scripts in the
same way, and the same rules apply.
@sect{Execution Model}
@p
Ammonite's Scala Scripts run as normal Scala code, though with some
simple source-to-source transformations applied first to turn the
script syntax (which allows top-level expressions, @hl.scala{def}s,
@hl.scala{val}s, etc.) into valid Scala syntax (which doesn't). What
happens is roughly:
@ul
@li
A script file is read off disk
@li
If the script file has been compiled/executed before, the previously
compiled bytecode and metadata is loaded from the
@code{~/.ammonite/cache}. If not, the script is parsed but @i{not
yet} compiled.
@li
@sect.ref{Multi-stage Scripts} are split into multiple individual
scripts, to be handled separately/sequentially.
@li
Any @sect.ref{Magic Imports} are resolved: any
@sect.ref("import $ivy", "Ivy/Maven dependencies are downloaded"), or any
@sect.ref("import $file", "imported scripts") are themselves run.
Any imported scripts are themselves handled in the same way, as are
any scripts @i{they} import, etc.
@li
If the script has already been previously compiled and cached, the
cached bytecode that was read off of disk earlier is executed.
@li
Otherwise, the source code for this script is then wrapped in a
@hl.scala{package}/@hl.scala{object} wrapper, corresponding to the
path to the script from the current script's enclosing folder.
For example, a script at path @code{foo/bar/baz/Qux.sc} will be
wrapped in:
@hl.scala
package foo.bar.baz
object Qux{
// script code
}
@li
The script is then compiled and executed.
@p
In general, due to Scala's slow compiler, Scala Scripts rely heavily
on caching to achieve reasonable performance. While the first run of a
modified script has a few-seconds overhead due to the Scala compiler,
subsequent runs of the same script should be fast-ish, with only a few
100ms overhead for spinning up a JVM.
@p
Although this is much slower than other scripting languages like Bash
(which starts up in ~4ms) or Python (~30ms), in practice it is
acceptable for many use cases. You probably do not want to
@code{find . | xargs amm Foo.sc} on large numbers of files, where
the 100ms overhead will add up, but for individual scripts it should
be fine.
@p
Furthermore, Ammonite makes it really easy to include that sort of
recursive/iterative logic inside a single script: you can use
@hl.scala{ls!} or @hl.scala{ls.rec!} from Ammonite-Ops to
traverse the filesystem and work on multiple files all within the same
process, which avoids duplicating the startup overhead on all the files
you are manipulating.
@sect{SBT Integration}
@p
If you have an existing SBT project and you'd like to access those
classes from an ammonite script, you can achieve this by running
your script through SBT itself. This requires adding ammonite to your
SBT project and creating a "bridge" class to pass arguments from SBT
into an ammonite @hl.scala{Main} class.
@p
Add the ammonite @ammonite.Constants.version dependency to @code{build.sbt}:
@hl.scala
libraryDependencies += "com.lihaoyi" % "ammonite" % "@ammonite.Constants.version" % "test" cross CrossVersion.full
@p
In your test directory, create a class like:
@hl.ref("integration"/"src"/"test"/"scala"/"ammonite"/"integration"/"AmmoniteBridge.scala")
@p
Run your script using @code{sbt "test:run /path/to/script.sc arg1 arg2 arg3"}.
@p
If you have already started an SBT repl, then you can run the above without the quotes:
@code{test:run /path/to/script.sc arg1 arg2 arg3}
@p
Running a script from your SBT project can be achieved with
@code{ammonite.AmmoniteMain.main(Array("--predef-code", """println("welcome!")""", "file.sc"))}