Syntax
Simple literals
Bex script uses the same syntax as Java for simple literals.
Numeric, character an boolean literals are exactly like in Java.
See
Java Language Specification for details.
Valid literals are for example:
bex> 0xf
15
bex> 1127498877973L
1127498877973
bex> 1.2f
1.2
bex> 1.3e3
1300.0
bex> 'a'
a
bex> true
true
Bex script supports the same syntax for string literals as Java.
However in addition to Java like string literals, bex script supports
also multiline string literals. For example:
bex> "hello world"
hello world
bex> """hello
multiline
world
"""
hello
multiline
world
Note that the normal character escapes are not supported in multiline
string literals.
From version 1.1, bex also supports embedded expressions inside strings.
For example:
bex> "Current date is: ${Date.new()}"
Current date is: Sun Oct 01 18:28:46 EEST 2006
Embedded expressions work both with normal strings and with multiline
strings.
Bex script has two additional special literals: null and void.
Null is the value of a reference that does not refer to any object.
Void is the value of an expression that does not have any value.
Variables
The variables in bex script are untyped. A variable has a name
and a value. A variable is defined by assigning a value to a name.
For example:
bex> a = 5
5
bex> hello = "world"
world
bex> a
5
Variables live inside containers called environments. An environment
is basically a Map that maps variable names to values. In addition an
environment is associated with a parent environment unless it is the
top environment. The parent of the top environment is null.
Special variables
In addition to user defined variables bex script has the following
"magic" variables:
this |
Refers to the environment of the currently
executing function. This is the environment where the function call
arguments and other "local" variables live. |
super |
Refers to the parent of an environment. |
$context |
Refers to the evaluation context. The evaluation
context provides access to the script call stack. For example
$context.caller() returns the environment of the caller of the currently
executing function. |
$0, $1, ... |
Refers to the arguments of a function that
were not declared when the function was defined |
$args |
Refers to the array containing the command line
arguments. |
Identifiers
Bex script variables are identified by name. Other identifiers
in bex script are Java class names and java class member names.
Valid bex script variable names begin with a letter [A-Za-z] or
character underscode '_' or dollar sign '$'. The rest of the
characters are either letters, digits, underscores or dollars.
The syntax for Java class names differs from Java. The package
name separator is double colon '::' and the inner class name separator
is the dollar sign '$'.
So for example:
bex> java::util::Map$Entry
interface java.util.Map$Entry
The syntax for referencing the members of java objects and classes
is the same as in Java, for example:
bex> java::lang::System.out.println("hello")
hello
void
The reason not to use '.' as the separator is that I don't like
using the same token for both member access and as a name separator.
Having ambiguous tokens like that just feels dirty - and makes the
implementation messy.
Expressions
A bex script program is a sequence of expressions. A program is
executed by the bex script interpreter by evaluating the expressions in
order. Expressions are built from literals, variable names and Java
class and class member names by combining them using operators.
Logical operators
== | a == b |
True if a equals b. For Java objects the comparison is done
with the Object.equals method.
|
!= | a != b |
True if a does not equal to b. This
is equivalent with !(a == b).
|
&& | a && b |
True if a both a and b evaluate to
boolean true. |
|| | a || b |
True if either a or b evaluate
to boolean true. |
! | !a |
True if a evaluates to boolean false. |
Arithmetic operators
+ | a + b |
Addition of a and b. If a is a string then
concatenates b to a. |
- | a - b |
Subtraction of b from a. |
* | a * b |
Multiplication of a and b. |
/ | a / b |
Division of a and b. |
% | a % b |
Modulus of a and b
(the remainder of a divided by b). |
Bitwise operators
& | a & b |
Bitwise and of a and b. |
| | a | b |
Bitwise or of a and b. |
^ | a ^ b |
Bitwise xor of a and b. |
<< | a << b |
Bitwise shift of a by b bits to left. |
>> | a >> b |
Signed bitwise shift of a by b bits to right. |
>>> | a >>> b |
Unsigned bitwise shift of a by b bits to right. |
~ | ~a |
Bitwise negation of a. |
Assignment operators
For all binary assignment operators the value of the assignment
expression is assigned to the left hand side of the expression.
= | a = b |
Assigns b to a. |
+= | a += b |
Addition of a and b. If a is a string then
concatenates b to a. |
-= | a -= b |
Subtraction of b from a. |
*= | a *= b |
Multiplication of a and b. |
/= | a /= b |
Division of a and b. |
%= | a %= b |
Modulus of a and b. |
&= | a &= b |
Bitwise and of a and b. |
|= | a |= b |
Bitwise or of a and b. |
^= | a ^= b |
Bitwise xor of a and b. |
<<= | a <<= b |
Bitwise shift of a by b bits to left. |
>>= | a >>= b |
Signed bitwise shift of a by b bits to right. |
>>>= | a >>>= b |
Unsigned bitwise shift of a by b bits to right. |
++ | a++ ++a |
Incrementation of a by one. The value of the suffix form
is the value of a before the incrementation. The value of the prefix form
is the value of a after the incrementation. |
-- | a-- --a |
Subtraction of one from a. The value of the suffix form
is the value of a before the subtraction. The value of the prefix form
is the value of a after the subtraction. |
Special operators
. | a.b |
The member b of a. If a is a bex script environment
then a.b is the value of the variable with name b. If a is a Java object or class
then a.b is the value of the class member with name b. |
[] | a[b] |
"Lookup" of b from a. If a is a Java array, b is expected
to be an integer and the value of a[b] is the array element with index b. If a
is an instance of java.util.Map then a[b] is equivalent with a.get(b).
If a is a bex script environment then a[b] is the value of variable with
name b. |
() | a(b,c) |
The return value of calling a with arguments
b and c. |
Block expressions
Functions are created in bex script by assigning a block expression to
a variable. A block expression consists of an optional declaration of
unbind variables and a sequence of expressions forming the body
of the block expression. For example:
bex> a = |x,y|{ print(x + y) }
bex.Block@67ac19
bex> a("hello","world")
helloworld
void
The unbind variables (arguments) are declared inside a pair of vertical
bar '|' characters and separated by commas. The argument declaration is
optional. Any arguments that are not declared will be available to the
body expressions as special variables $0, $1 and so on. If the last argument
ends with token '...' the argument will contain the rest of the arguments as an
array. For example:
bex> a = { print($0) }
bex.Block@253498
bex> a("hello")
hello
void
bex> a = |args...|{ args }
bex.Block@9fef6f
bex> b = a(1,2,3,4,5)
[Ljava.lang.Object;@1a457b6
bex> Arrays.asList(b)
[1, 2, 3, 4, 5]
Function call
A block expression can be called with the special operator (). When called
like this the block expressions effectively behaves like a regular function:
a new environment is created, the unbind variables are bound according to the
argument declaration of the block expression and finally the expressions
forming the body are evaluated. The following text uses the terms block
expression and function interchangeably. Note however that block expressions
also support other operations in addition to the call-operation.
Normally the arguments to a block expression are specified as a comma separated
list inside the parentheses. In addition to this, bex script provides special
syntactic sugar for arguments that are block expressions. They can be specified
outsidethe parentheses and commas are not needed between them. If all the
arguments are block expressions then the parentheses are not needed at all.
This syntactic trick allows writing bex script functions that are syntactically
similar to some control flow statements in Java.
The following example illustrates different ways to call a block expression.
In the example the construct {void} is just an example function literal.
bex> fun = |args...|{ print(Arrays.asList(args)) }
bex.Block@2b58b0
bex> fun(1,2,3)
[1, 2, 3]
void
bex> fun(1,2,{void})
[1, 2, bex.Block@275860]
void
bex> fun(1,2) {void}
[1, 2, bex.Block@2d8e70]
void
bex> fun {void} (2) {void}
[bex.Block@2d8890, 2, bex.Block@2d8880]
void
bex> fun {void} {void} {void}
[bex.Block@2d8320, bex.Block@2d8310, bex.Block@2d8300]
void
Normally a call terminates when all the expressions of the
block expression body have been evaluated. A call can also terminate
before that if an expression in the block expression body evaluates to an
instance of bex.ReturnControl. The builtin functions return, break and continue
evaluate to bex.ReturnControl instances, so they can be used as control
flow statements.
Another way a block expression can be evaluated is by inlining it.
Inlining a block expression is very much like calling it. The only differences
are in the way "this" and "super" are resolved and how it behaves with respect
to ReturnControls. In a normal call, if an expression evaluates to an instance
of ReturnControl, the interpreter stops evaluating the expressions and the
value of the call is the value associated with the ReturnControl. However, when
the block expression is being inlined the value of the inlining is not the
value associated with the ReturnControl object but the ReturnControl object
itself. So if block expression b is being inlined inside a block expression a
and b calls return(1), then also a returns and the return value is 1. This
feature can be used to implement loops and other control structures that are
often implemented as special statements in other languages.
Special variables "this" and "super" are normally resolved so that this
resolves to current environment and super to the lexical parent environment.
When a block expression is inlined, however, they are resolved to the closest
non-inlined environment and its parent. This means that you can do:
if (true) { this.foo = "bar" }
print(this.foo)
and expect it to print "bar" instead of void because if inlines the block
expression it receives as its second argument instead of calling it. The
builtin function
inline can be used to create function from a block
expression that will be inlined when the expression is called.
There is yet another way to use a block expression which is especially
useful when constructing domain specific languages. In addition to calling
and inlining a block expression can be just evaluated. This is different from
calling/inlining because no new environment is created. The effect is the same
as if the body of the block expression was written right there where it is
being evaluated. If the block expression had any unbind variables they will
be resolved according to normal variable resolution procedure and if they are
unresolved an exception is thrown. The builtin function eval can be
used to evaluate a block expression.
Scripted objects and meta object protocol
To create a scripted object (i.e. an entity with attributes and functions
that manipulate those attributes) one needs to just return (or pass) a reference
to an environment. The returned environment behaves like a closure: it
contains all the variable bindings that existed before it was returned. Now
this is very much like in BeanShell and while extremely useful not particularily
exciting. However, bex script also provides a powerful meta object protocol (MOP)
that is more flexible than BeanShell's invoke. By using special variables
$set, $get and $dir one can extend the regular variable
resolution procedure and do all kinds of clever tricks.
For example to make javax.servlet.http.HttpSession to appear as a bex script
object, code similar to the following example could be used:
wrapSession = |session|{
ws = object()
ws.$get = |name|{ session.getAttribute(name) }
ws.$set = |name, value|{ session.setAttribute(name, value) }
ws.$dir = { session.getAttributeNames() }
ws
}
Builtins
Bex script comes with a bunch of useful builtin functions.
These are defined in the script default.bex which is automatically
evaluated when the interpreter is started.
array |
array(byte,5) |
Creates a Java array of the type given as the
first argument, having the dimensions specified by the rest of the
arguments. For example array(byte, 5) is equivalent with new byte[5]
in Java.
|
cast |
cast(10, Double.TYPE) |
Casts the first argument to the class specified by the second argument.
|
dir |
dir(this) |
Lists the variable bindings in the specified environment. Returns
a collection containing the names of the variables in the specified
environment.
|
each |
each(list) { print($0) } |
Inlines the function specified by the second argument with every item
in the collection specified by the first argument. See also extensions
to standard Java collections.
|
eval |
eval { foo = "bar" } |
Evaluates the given block in the caller's environment. The example above
would introduce a variable "foo" to the caller's environment. This is
useful when creating DSLs.
|
if |
if (a < 0) {
print("negative")
} {
print("positive")
}
|
If the first argument is "condition true", the function
specified by the second argument is inlined. Otherwise, if there
is a third argument that is a function, it is inlined. Here
"condition true" is anything that is not: boolean false, null, void or
empty string.
|
import |
import("java::awt", javax::awt::event") |
Adds the packages given as arguments to the package search list.
When the interpreer resolves Java class names it searches the
packages in this package list for a class with the given name.
|
lock |
lock(obj) { obj.foo++ } |
Synchronizes to the first argument and then inlines the second argument.
This is equivalent with Java's synchronized(obj) { obj.foo++; } statement.
|
throw |
throw(Exception.new()) |
Throws the given exception. |
try |
try {
out.write(str.length())
} (NullPointerException) {
print("null string")
} (IOException) {
print("write failed: " + $0)
} {
out.close()
}
|
Inlines the function given as the first argument.
If this function throws an exception then try goes through
the rest of the arguments. If an argument matches the thrown
exception then the function specified by the next argument
is inlined. Finally if the last argument is a function
not associated with any exception then it is inlined after
everything else.
|
while |
while { a < 10 } {
print(a)
a++
}
|
While the first argument function evaluates to condition true
(see if for the definition of condition true), the second argument
function is inlined.
|
inline |
inline(print) |
Returns a function that inlines the given function.
|
print |
print("hello world") |
Prints the given string + newline to standard output.
|
list |
list(1,2,3,4) |
Creates a list that contains the arguments. The returned list
implements java.util.List interface.
|
tuple |
tuple(1,2,3) |
Creates a tuple of the arguments. The returned
object is an array that contains the arguments.
|
hash |
hash("name1","value1","name2","value2") |
Creates a HashMap with the given arguments. The arguments are
processed in pairs: the first is treated as the key and the
second as the value.
|
return |
return(1) |
Returns the given value from a function.
|
break |
break(1) |
Breaks the execution of a loop and returns the given value
as the value of the loop expression.
|
continue |
continue() |
Continues the execution of a loop.
|
object |
object { firstName = "Juha"; lastName = "Lindström" } |
Creates a scripted object by evaluating the body of the function given
as first argument. This is syntactic sugar for declaring and calling a function
that returns its own environment. For example, the above example is equivalent
with: { firstName = "Juha"; lastName = "Lindström"; this }()
|
implement |
implement(Runnable,Iterator,this) |
Creates a dynamic proxy that implements the specified interfaces by delegating calls to the given bex object. The last argument is either a bex object
or a block implementing the specified interfaces.
|
include |
include("script.bex") |
Evaluates the given file(s) in the current environment. The files are first
searched from the file system and then from the classpath.
|
Java extensions
Bex script supports registering extension methods to existing
Java classes. This functionality can be used to make accessing Java
APIs from scripts easier and more natural. The default startup script
default.bex contains the following extensions:
java::util::Collection.each |
bex> list(1,2,3).each |it|{ print("item: void") }
item: 1
item: 2
item: 3
void
|
Loops through the items in the collection and inlines the specified
function. The current item is passed to the function as the first
argument. This extension works also for Java arrays since bex script
treats them in the same manner as Collections with regards to extensions.
|
java::util::Iterator.each |
bex> list(1,2,3).iterator().each |it|{ print("item: void") }
item: 1
item: 2
item: 3
void
|
Loops through the items in the iterator and inlines the specified
function. The current item is passed to the function as the first
argument.
|
bex::Primitive.times |
bex> 3.times |i|{ print("count: void") }
count: 0
count: 1
count: 2
void
|
Loops from 0 to the target value and inlines the specified function.
|
java::io::InputStream.load |
bex> FileInputStream.new("test.txt").load()
[B@1ea2dfe
|
Loads all bytes from the stream and returns the data as a byte array.
|
java::io::Reader.load |
bex> FileReader.new("test.txt").load()
hello
world
|
Reads all characters from the reader and returns the data as a string.
|
java::io::File.load |
bex> File.new("test.txt").load()
line: hello
line: world
void
|
Loads the contents of the file and returns it as a byte array.
|
java::io::InputStream.eachLine |
bex> FileInputStream.new("test.txt").eachLine |l|{ print("line: void") }
line: hello
line: world
void
|
Inlines the given function for each line read from the reader.
The line is passed to the function as the first argument. Note that
the above example is flawed since it does not close the FileReader.
|
java::io::Reader.eachLine |
bex> FileReader.new("test.txt").eachLine |l|{ print("line: void") }
line: hello
line: world
void
|
Inlines the given function for each line read from the reader.
The line is passed to the function as the first argument. Note that
the above example is flawed since it does not close the FileReader.
|
java::io::File.eachLine |
bex> File.new("test.txt").eachLine |l|{ print("line: void") }
line: hello
line: world
void
|
Inlines the given function for each line in the file.
The line is passed to the function as the first argument.
|
java::io::InputStream.with |
FileInputStream.new("test.txt").with |in|{ in.read() }
|
Ensures that the InputStream is closed after the function is
inlined no matter what happens.
|
java::io::OutputStream.with |
FileOutputStream.new("test.txt").with |out|{ out.write(1) }
|
Ensures that the OutputStream is closed after the function is
inlined no matter what happens.
|
java::io::Reader.with |
FileReader.new("test.txt").with |in|{ in.read() }
|
Ensures that the Reader is closed after the function is
inlined no matter what happens.
|
java::io::Writer.with |
FileWriter.new("test.txt").with |in|{ out.write("foo") }
|
Ensures that the Writer is closed after the function is
inlined no matter what happens.
|
java::net::Socket.with |
Socket.new("localhost", 80).with |s|{
s.outputStream.with { $0.write("GET / HTTP/1.0\r\n\r\n".bytes) }
s.inputStream.with { InputStreamReader.new($0).eachLine { print($0) } }
}
|
Ensures that the Socket is closed after the function is
inlined no matter what happens.
|
java::net::ServerSocket.with |
ServerSocket.new(4314).with |ss|{ handle(ss.accept()) }
|
Ensures that the ServerSocket is closed after the function is
inlined no matter what happens.
|
Adding new Java extensions is easy: just declare a new function
and assign it to a static member of a Java class that does not exist.
The first argument to the function will be the object that received
the call. For example:
bex> String.toInt=|s|{ Integer.parseInt(s) }
bex.Block@19fcc69
bex> "123".toInt() + 1
124
Interactive use
The bex script interpreter can be started in interactive mode
with the following command:
java bex.Interpreter [-q] [-i] [file1] [arg1 arg2 ...]
If there are no command line arguments the interpreter starts in
interactive mode. Option -i forces the interpreter to enter interactive
mode even if the arguments also specify a file "file1" to be evaluated.
The rest of the arguments arg1 arg2 ... are passed to the script as
an array of strings with name $args. Option -q tells the interpreter to be
quiet and not print any prompts or welcome messages.
Tip: You can get get command history and other shell like features
by using a very nice utility program called
rlwrap. Just start
the interpreter with rlwrap java bex.Interpreter and working with the
interpreter will be much more pleasant.
Tip2: When used from the command line, the interpreter evaluates
file $HOME/.bexrc if it exists. You use this facility to setup the root
environment the way you want (e.g. by adding some functions not provided by
the default environment).
Embedded use
The interpreter can be embedded inside a Java application in
the following way:
// Create the interpreter
bex.Interpreter i = new bex.Interpreter();
// Sets a variable inside the interpreter
i.put("myname", "bex");
// Evaluate file myscript.bex
FileReader in = new FileReader("myscript.bex");
try { i.eval(fr); } finally { in.close(); }
// Evaluate a string
i.eval("greeting = \"hello \" + myname");
// Print the value of the script variable "greeting"
System.out.println(i.get("greeting"));
Use from within a shell script
The following trick can be used to run a bex script from within a shell
script:
#!/bin/bash
export CLASSPATH=/home/jli/projects/bexscript/trunk/dist/bex-1.0.1.jar
java bex.Interpreter -q <<EOF
10.times { print("hello from bexscript") }
EOF
The -q switch causes the interpreter not to output any prompts or
welcome messages when reading input from stdin.