Yet Another Expect for Java
Overview
ExpectIt - is yet another pure Java 1.6+ implementation of the Expect tool. It is designed to be simple, easy to use and extensible. Written from scratch. Here are the features:
- Fluent-style API.
- No third-party dependencies.
- NIO based implementation using pipes and non-blocking API.
- Extensible matcher framework. Support regular expressions and group operations.
- Support multiple input streams.
- Support 'interact' loop.
- Extensible filter framework to modify input, for example, to remove non-printable ANSI terminal characters.
- Custom Expect Ant Task.
- Tested on Andriod.
- Apache License.
The ExpectIt project is a modern alternative to other popular 'Expect for Java' implementations, such as:
I believe that none of the projects above has all the features that ExpectIt has. So if you are looking for a Java expect library please give ExpectIt a try.
The API javadoc documentation is available here.
Quick start
The library is available on the Maven central. Add the following Maven dependency to your project:
<dependency>
<groupId>net.sf.expectit</groupId>
<artifactId>expectit-core</artifactId>
<version>0.9.0</version>
</dependency>
You can also download the expectit-core.jar
file from the release project page at sourceforge.net and add it to your classpath.
To begin with you need to construct an instance of net.sf.expectit.Expect
and set the input and output streams as follows:
// the stream from where you read your input data
InputStream inputStream = ...;
// the stream to where you send commands
OutputStream outputStream = ...;
Expect expect = new ExpectBuilder()
.withInputs(inputStream)
.withOutput(outputStream)
.build();
expect.sendLine("command").expect(contains("string"));
Result result = expect.expect(regexp("(.*)--?--(.*)"));
// accessing the matching group
String group = result.group(2);
Note that you may need to add static import of the matcher factory methods in your code.
How it works
Once an Expect object is created the library starts background threads for every input stream. The threads read bytes from the streams and copy them into NIO pipes. The pipes are configured to use non-blocking source channel.
The expect object holds a String buffer for each input. The user calls one of the expect methods to wait until the given matcher object matches the corresponding buffer contents. If the input buffer doesn't satisfy the matcher criteria, then the method blocks for a configurable timeout of milliseconds until new data is available on the input stream NIO pipe.
The result object indicates whether the match operation was successful or not. It holds the context of the match. It implements the java.util.regexp.MatchResult
interface which provides access to the result of regular expression matching results. If the match was successful, then the corresponding input buffer is update, all characters before the match including the matching string are removed. The next match is performed for the updated buffer.
Thread safety notes
The send methods are generally thread safe as long as the underlying output streams are. In other words it is safe to send data from one thread and expect the results in another.
The expect methods are not thread safe since they mutate the state of the expect buffers. The expect operaiton must not be performed concurrently.
Interacting with OS process
Here is an example of interacting with a spawn process:
Process process = Runtime.getRuntime().exec("/bin/sh");
Expect expect = new ExpectBuilder()
.withInputs(process.getInputStream())
.withOutput(process.getOutputStream())
.withTimeout(1, TimeUnit.SECONDS)
.withExceptionOnFailure()
.build();
// try-with-resources is omitted for simplicity
expect.sendLine("ls -lh");
// capture the total
String total = expect.expect(regexp("^total (.*)")).group(1);
System.out.println("Size: " + total);
// capture file list
String list = expect.expect(regexp("\n$")).getBefore();
// print the result
System.out.println("List: " + list);
expect.sendLine("exit");
// expect the process to finish
expect.expect(eof());
// finally is omitted
process.waitFor();
expect.close();
Interacting with SSH server
Here is an example on how to talk to a public SSH service on http://sdf.org using the JSch library. Note: you will to add the jsch library to your project classpath.
JSch jSch = new JSch();
Session session = jSch.getSession("new", "sdf.org");
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect();
Channel channel = session.openChannel("shell");
channel.connect();
Expect expect = new ExpectBuilder()
.withOutput(channel.getOutputStream())
.withInputs(channel.getInputStream(), channel.getExtInputStream())
.withEchoOutput(System.out)
.withEchoInput(System.err)
// .withInputFilters(removeColors(), removeNonPrintable())
.withExceptionOnFailure()
.build();
try {
expect.expect(contains("[RETURN]"));
expect.sendLine();
String ipAddress = expect.expect(regexp("Trying (.*)\\.\\.\\.")).group(1);
System.out.println("Captured IP: " + ipAddress);
expect.expect(contains("login:"));
expect.sendLine("new");
expect.expect(contains("(Y/N)"));
expect.send("N");
expect.expect(regexp(": $"));
} finally {
expect.close();
channel.disconnect();
session.disconnect();
}
Note that SSH servers normally echo the received commands. The echo can be disabled by sending the stty -echo
command. This is an example of capturing the result of the pwd
command when the command echo is switched off.
Using different type of matchers
In the following example you can see how to combine different matchers (assuming static import of matcher factory methods):
// match any of predicates
expect.expect(anyOf(contains("string"), regexp("abc.*def")));
// match all
expect.expect(allOf(regexp("xyz"), regexp("abc.*def")));
// varargs method arguments are equivalent to 'allOf'
expect.expect(contains("string1"), contains("string2"));
// expect to match three times in a row
expect.expect(times(3, contains("string")));
// expect any non-empty string match
expect.expect(anyString());
// expect to contain "a" and after that "b"
expect.expect(sequence(contains("a"), contains("b")));
Filtering the input
If you want to modify or remove some characters in the input before performing expect operations you can use filters. A filter instance implements net.sf.expectit.filter.Filter
interface and is applied right before the matching occurs.
Filters are defined at the time an net.sf.expectit.Expect
instance is being created and they can be disabled and re-enabled while working with the Expect instance.
The library comes with the filters for removing ANSI escape terminal and non-printable characters. There are also more general replaceInString
and replaceInBuffer
filters used to modify the input buffer using regular expressions. Here is an example:
Expect expect = new ExpectBuilder()
.withOutput(...)
.withInputs(...)
// define the filters
.withInputFilters(
// set the filter to remove ANSI char for colors in terminal
removeColors(),
// set the filter to remove non-printable characters
removeNonPrintable(),
// set the filter to replace a substring that matches
// the regular expression
replaceInString("a(.)c", "x$1z"))
.build();
Note that you may need to add static import of the filter factory methods in your code.
Please be careful about the order you declare filters in.
Filters are called in the order they were declared in.
For example, removeNonPrintable()
removes \e
(\033
or \x1B
), which is the ESC character.
ANSI color sequences however uses \e
as part of the color sequence.
So if you declare the removeNonPrintable()
filter before the removeColors()
filter, removeColors()
filter will NOT work.
More examples
- Socket Example: parsing HTTP response
- Complete SSH example using JSch
- Complete SSH example using SshJ
- Interacting with the Apache Karaf remote shell
- Expect for Ant example
- Capture the command result from SSH session
- Disable SSH server echo
- Interact loop example with Java 8
- Telnet client example
Questions
If you have any questions about the library please post a message to this Google group. You can also ask a question on stackoverflow with hash tag expectit.