Commandline Scripts
- Command Line Arguments
- Jactl Shell Script
- Running Scripts
- The -e Option
- Printing Result
- The -V Option
- The -v Option
- The -d Option
- Input
- The -p and -n Options
- BEGIN and END Sections
- Passing Arguments to Script
.jactlrc
File
Jactl scripts can be run from the shell command line and can be used in situations where you might normally
use Unix utilities such as sed
or awk
and even some situations where you might have used perl
in the past.
The Jactl JAR file, when run using java -jar
will compile and run a given script.
Command Line Arguments
If you run the JAR file without any arguments it prints this help text:
$ java -jar jactl-2.1.0.jar
Usage: jactl [options] [programFile] [inputFile]* [--] [arguments]*
-p : run in a print loop reading input from stdin or files
-n : run in a loop but do not print each line
-e script : script string is interpreted as jactl code (programFile not used)
-v : show verbose errors (give stack trace)
-V var=value : initialise jactl variable before running script
-d : debug: output generated code
-h : print this help
Exception in thread "main" java.lang.IllegalArgumentException: Missing '-e' option and no programFile specified
at io.jactl.Jactl.main(Jactl.java:52)
Jactl Shell Script
It is recommended that you create a shell script for invoking Jactl called jactl
to save having to type the
java -jar <path_to>/jactl-2.1.0.jar
every time.
For a shell compatible with bash
you can create a shell script like the following called jactl
and add it to
a directory in your execution path:
#!/bin/bash
java -jar <path_to>/jactl-2.1.0.jar "$@"
Note that the /bin/bash
should be the location of your shell and <path_to>
should be replaced with the location
of the jactl-2.1.0.jar
file.
We will assume from now on that you have such a shell script and will use jactl
in place of java -jar jactl-2.1.0.jar
.
Running Scripts
To run a file containing a Jactl script just pass the file name directly to the jactl
command.
For example, assume there is a file myscript.jactl
containing this:
def fact(x) { x <= 1 ? 1 : x * fact(x - 1) }
10.map{ it + 1 }
.map{ [it, fact(it)] }
.each{ n,fact -> println "Factorial of $n is $fact" }
To run this script:
$ jactl myscript.jactl
Factorial of 1 is 1
Factorial of 2 is 2
Factorial of 3 is 6
Factorial of 4 is 24
Factorial of 5 is 120
Factorial of 6 is 720
Factorial of 7 is 5040
Factorial of 8 is 40320
Factorial of 9 is 362880
Factorial of 10 is 3628800
The -e Option
The -e
option allows you to run some Jactl code entered directly on the command line:
$ jactl -e '10.map{ it + 1 }.sum()'
55
Note that -e
only accepts a single argument so you should wrap the code in '
single quotes so that the shell
won’t split the code into multiple arguments when a space is encountered.
If you want to be able to use single quotes in your script then in bash
, if the first quote is prefixed by $
, you
are then allowed to use \
to escape other characters within the single quotes (including single quotes):
$ jactl -e $'10.map{ (int)\'a\' + it }.map{ it.asChar() }.join()'
abcdefghij
Printing Result
By default, when not using the -p
or -n
options (see below), Jactl will output the value of the final expression
in the Jactl script or command line script:
$ jactl -e '3 + 4'
7
If the script itself already invokes print
or println
then it assumes that the script wants to control its output
and the default printing of the final expression is suppressed:
$ jactl -e 'println "Result is ${3 + 4}"'
Result is 7
The -V Option
The -V
option allows you to define a variable and a string value for that variable that the script can access:
$ jactl -V n=10 -e '(n as int).map{ (it+1).sqr() }'
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
The option can be used multiple times:
$ jactl -V n=10 -V power=3 -e '(n as int).map{ (it+1).pow(power as int) }'
[1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]
See Passing Arguments to Script for another way to pass in values from the command line to a script.
The -v Option
The -v
option is used when there are errors to show more verbose output.
This means that the error stack trace will be shown along with the error where as
normally only the error message is shown.
The -d Option
The -d
option is the debug option to print out the generated code.
If given twice it outputs even more detail.
This option should only be used when reporting problems.
Input
The Jactl scripts can read from stdin
using nextLine()
.
Assume there is a file called some_file
with this content:
This is the first line in a file
This is the second line in the same file
This is the third line
Then, to prepend the line length to each line we can cat
the file to our jactl script:
$ cat some_file | jactl -e 'def line; while ((line = nextLine()) != null) { println "${line.size()}: $line" }'
32: This is the first line in a file
40: This is the second line in the same file
22: This is the third line
This could also be written more idiomatically as:
$ cat some_file | jactl -e 'stream(nextLine).each{ println "${it.size()}: $it" }'
32: This is the first line in a file
40: This is the second line in the same file
22: This is the third line
Jactl supports a list of files being passed to it on the command line so this achieves the same thing:
$ jactl -e 'stream(nextLine).each{ println "${it.size()}: $it" }' some_file
32: This is the first line in a file
40: This is the second line in the same file
22: This is the third line
Multiple file names can be listed and the script will concatenate the multiple files into one stream of input.
Assume that there is another file called another_file
with this content:
this is some text in another file
and this is another line in that file
Then if we pass both file names to the script we will have this output:
$ jactl -e 'stream(nextLine).each{ println "${it.size()}: $it" }' some_file another_file
32: This is the first line in a file
40: This is the second line in the same file
22: This is the third line
33: this is some text in another file
37: and this is another line in that file
When file names are passed to the script, the stdin
input is not read.
If you want to include stdin
in the input processed by the script you use a single hyphen -
as the file name.
For example:
$ jactl -e 'stream(nextLine).each{ println "${it.size()}: $it" }' some_file - another_file
32: This is the first line in a file
40: This is the second line in the same file
22: This is the third line
text typed in at the terminal
29: text typed in at the terminal
and more text
13: and more text
33: this is some text in another file
37: and this is another line in that file
Note that lines not starting with a number were typed in at the terminal and so form the input for stdin
.
To terminate the stdin
input <ctrl-D>
was used.
Since the -
file name was the second one in the list the stdin
input was added between the two files.
The -p and -n Options
The -p
option causes the script to be wrapped in the following code:
def it
while ((it = nextLine()) != null) {
... // your script inserted here
println it
}
This means that the it
variable will iterate over the lines of the input (whether stdin
or files listed on command
line).
For example, given the files we saw in the previous section called some_file
and another_file
, we can do this to
prepend the length to every line:
$ jactl -p -e 's/^/${it.length()}: /' some_file another_file
32: This is the first line in a file
40: This is the second line in the same file
22: This is the third line
33: this is some text in another file
37: and this is another line in that file
This works because s/^/${it.length()}: /
by default operates on the it
variable and so it modifies it
which is
then automatically printed by the implicit println it
in the wrapping code.
Note that when there are multiple options, they can be joined together (unless they take an argument) so
-p -e 's/^/${it.length()}: /'
can also be written -pe 's/^/${it.length()}: /'
since -p
doesn’t take an
argument.
If you don’t want to automatically print every line then the -n
has the same behaviour except that it doesn’t
automatically print the line each time, so in order to print only lines that match some regex we could do this:
$ jactl -ne '/ s.*file/r and println it' some_file another_file
This is the second line in the same file
this is some text in another file
To print lines that do not match a regex, this would work:
$ jactl -ne '/ s.*file/r or println it' some_file another_file
This is the first line in a file
This is the third line
and this is another line in that file
Or another way to print lines that don’t match:
$ jactl -ne 'println it unless / s.*file/r' some_file another_file
This is the first line in a file
This is the third line
and this is another line in that file
And yet another way:
$ jactl -ne '!/ s.*file/r and println it' some_file another_file
This is the first line in a file
This is the third line
and this is another line in that file
Note that these options can also be used when the script is in a separate file rather than on the command line.
So imagine we have a script called freq.jactl
that counts letter frequency for a line:
println it.filter{ it != " " }
.reduce([:]){ m,c -> m[c]++; m }
.sort{ a,b -> b[1] <=> a[1] }
.map{ c,freq -> "$c:$freq" }
.join(" ")
To run this over all lines of our input files we do this:
$ jactl -n freq.jactl some_file another_file
i:6 s:3 e:3 h:2 t:2 f:2 l:2 n:2 T:1 r:1 a:1
e:6 i:5 s:4 h:3 n:3 t:2 l:2 T:1 c:1 o:1 d:1 a:1 m:1 f:1
i:4 h:3 s:2 t:2 e:2 T:1 r:1 d:1 l:1 n:1
t:4 i:4 e:4 s:3 h:2 o:2 n:2 m:1 x:1 a:1 r:1 f:1 l:1
i:5 n:4 t:4 a:3 h:3 e:3 s:2 l:2 d:1 o:1 r:1 f:1
Since the script is wrapped in a while
loop, it is possible for the script to use continue
and break
where
it makes sense to control the behaviour of the loop:
$ jactl -pe '/second/r and continue; s/This is//' some_file another_file
the first line in a file
the third line
this is some text in another file
and this is another line in that file
Note in this example how the continue
skips the processing and the printing if the input line matches /second/
.
BEGIN and END Sections
When running with the -p
or -n
options you might want to do some initialisation at the very beginning or print
out a result at the very end of the input.
You can use a BEGIN
section to perform any up-front initialisation before any input is processed and an END
section to perform any work after the input has all been processed.
Here is an example where we modify the freq.jactl
script to add a BEGIN
section to initialise a Map that will
keep letter frequencies across all lines, and an END
section to print the results at the end:
BEGIN {
freq = [:]
}
println it.filter{ it != " " }
.reduce([:]){ m,c ->
m[c]++
freq[c]++ // update global frequency count
m
}
.sort{ a,b -> b[1] <=> a[1] }
.map{ c,freq -> "$c:$freq" }
.join(" ")
END {
println "\nTotals: " + freq.sort{ a,b -> b[1] <=> a[1] }
.map{ c,freq -> "$c:$freq" }
.join(" ")
}
Then when we run it:
$ jactl -n freq.jactl some_file another_file
i:6 s:3 e:3 h:2 t:2 f:2 l:2 n:2 T:1 r:1 a:1
e:6 i:5 s:4 h:3 n:3 t:2 l:2 T:1 c:1 o:1 d:1 a:1 m:1 f:1
i:4 h:3 s:2 t:2 e:2 T:1 r:1 d:1 l:1 n:1
t:4 i:4 e:4 s:3 h:2 o:2 n:2 m:1 x:1 a:1 r:1 f:1 l:1
i:5 n:4 t:4 a:3 h:3 e:3 s:2 l:2 d:1 o:1 r:1 f:1
Totals: i:24 e:18 s:14 t:14 h:13 n:12 l:8 a:6 f:5 r:4 o:4 T:3 d:3 m:2 c:1 x:1
Note that when using -p
and -n
, global variables do not have to be declared before they can be used.
Normally we would declare the freq
variable in the BEGIN
section like this:
BEGIN {
def freq = [:]
}
If we do that, however, the scope of freq
is the block within which it is declared, so it would not be visible outside
the BEGIN
block.
That is why, when running with -p
and -n
options, global variables do not need to be declared:
assigning to a global variable will cause it to be created.
Passing Arguments to Script
As well as passing in variables using the -V
option, you can additionally pass in arguments for a script at the
command line by using --
(double hyphen) and then listing the arguments.
The --
tells Jactl that there are no more options for Jactl itself (including no more file names) and that
all arguments after the --
should be passed as an array in a variable called args
that the script can then
access.
For example:
$ jactl -e 'args.each{ println "arg: $it" }' -- additional args
arg: additional
arg: args
.jactlrc
File
At start up time, the contents of ~/.jactlrc
are read.
This file, if it exists, is itself a Jactl script and allows you to customise the behaviour of the Jactl command
line scripts by setting the values of some global variables.
This file is also used by the Jactl REPL.
At the moment, all the variables relate to allowing you to customise Jactl by providing your own execution environment (for your event-loop specific application environment) and your own functions/methods. The values of the following variables can be set:
Variable | Type | Default Value | Description |
---|---|---|---|
environmentClass |
String | io.jactl.DefaultEnv |
The name of the class which will is used to encapsulate the Jactl runtime environment. See Integration Guide for more details. |
extraJars |
List | [] |
A list of file names for any additional JARs that should be added to the classpath. |
functionClasses |
List | [] |
A list of classes having a static method called registerFunctions(JactlEnv env) that will be invoked at start up. This allows you to dynamically add new functions (from one of the extraJars files) to the Jactl runtime. |
For example, there is a jactl-vertx project for use when integrating
with a Vert.x based application.
It uses a specific JactlVertxEnv
environment that delegates event scheduling to Vert.x and provides some
global functions that deal with distributed maps and an example function for sending/receiving JSON messages over HTTP.
Since the sendReceiveJson()
functions is provided as an example, it lives in the test jar of the jactl-vertx
project.
So to include all of these additional functions in your Jactl REPL or Jactl command line scripts you need to list
these two jars in the extraJars
list.
Note
Thejactl-vertx
test jar is built as a “fat” jar and includes the dependencies it needs (including the Vert.x libraries) so we don’t need to separately list the Vert.x jars as well.
To register these additional functions with the Jactl runtime we need to have created classes that have
a static method called registerFunctions(JactlEnv env)
which do the registration of the functions.
We then need to tell the runtime the name of these classes so that these static methods can be invoked which
will in turn register the functions.
For the jactl-vertx
library, there are two classes that handle the registration of these functions/methods:
jactl.vertx.VertxFunctions
jactl.vertx.example.ExampleFunctions
We therefore need to list these classes in the functionClasses
list of our .jactlrc
file.
If the jars are located under ~/projects/jactl-vertx/build/libs
then a .jactlrc
file that allows
the Jactl REPL and the Jactl commandline script execution to use Vert.x and these new functions would look like this:
environmentClass = 'jactl.vertx.JactlVertxEnv'
extraJars = [ '~/projects/jactl-vertx/build/libs/jactl-vertx-2.1.0.jar',
'~/projects/jactl-vertx/build/libs/jactl-vertx-2.1.0-tests.jar' ]
functionClasses = [ 'jactl.vertx.VertxFunctions',
'jactl.vertx.example.ExampleFunctions' ]
Note
The use of~
in the file names will be replaced with the location of the current user’s home directory.
Since the file is Jactl code we could also write it like this:
def VERSION = '2.1.0' // The jactl-vertx version to use
def LIBS = '~/projects/jactl-vertx/build/libs' // Location of the jars
// Specify the Vertx environment class to use
environmentClass = 'jactl.vertx.JactlVertxEnv'
// List the extra jactl-vertx jars
extraJars = [ "$LIBS/jactl-vertx-${VERSION}.jar",
"$LIBS/jactl-vertx-${VERSION}-tests.jar" ]
// List the function registration classes
functionClasses = [ 'jactl.vertx.VertxFunctions',
'jactl.vertx.example.ExampleFunctions' ]
Note
The extra jars can also be provided via the normal way of specifying them in your Java classpath if you prefer to do it that way.
To integrate with additional libraries, just add the jars to the extraJars
list and add any function
registration classes to the functionClasses
list.