HTTPS SSH

License: LGPL v3 Run Status Version Coverage

This package provides a simple Scala implementation for Expect, http://expect.sourceforge.net. More specifically it provides an implementation of the send and expect commands. For a throrough description of Expect and complete documentation, see http://expect.sourceforge.net

Overview

Objective

The objective is to programmatically interact with an external process inside Scala code. This makes it possible to use programs or scripts that are available on the host OS. The most common example is to use a shell script to perform some computations and collect the results. For instance, one may want to use ls to collect a list of files in a directory, or ps to determine which processes are currently running. In the Expect world, this is done by spawning a process (that runs ls), and then interacting with it via the stdin and sdout channels.

Send and Expect

The main problem to address is to detect that the output is ready to collect. This means we need to be able to determine when the external process has finished its computation on a given input. By convention in Expect, the termination of a computation of the external process is signaled by the a sentinelle token: the prompt. appears in the output channel. Overall, a typical interaction is 1) sending data to the process, and can be done using the input channel (e.g. stdin) and 2) waiting for a string terminated by the prompt to appear on the output channel (e.g. stdout).

The Expect package provides an interface to achieve this via the send and expect methods. The only assumption is that the process' responses are postfixed with the prompt. To interact more than one time, we can send a command, collect the ouput which is a string followed by an expected prompt, and start again.

The Prompt

The prompt can be defined by a simple regular expression. A typical interaction with the external process is a sequence of send/expect commands: 1. send a command to the process. 2. wait for a sequence of characters (including spaces/newlines) followed by the prompt.

The wait is implemented by the expect command. This command has a timeout (duration), and if the prompt does not appear on the process stout channel before the timeout has expired, the command returns a timeout failure. If the prompt appears within the allowed time bound (set by the timeout), the string that preceedes the prompt is returned by expect. Notice that the expect method terminates either when the prompt is seen on the outout channel or when the timeout expires, but no later.

As the process we interact with may be non-terminating it is important to understand that the prompt may not be 'suffix-closed'. For instance a prompt defined by the regular expression bash>(\n), i.e. the string ''bash>'' followed by an arbitrary number of newlines may result in a timeout even if the process outputs ''bash>'' within the time bound. Indeed the expect command is waiting for the longest string matching the regular expression that defines the prompt. After receiving ''bash>'', the next character on the output channel of the process could be a newline, and the expect command will keep waiting for the next character to extend the prompt matching as much as it can.

A prompt like ''bash>'' will not exhibit the same problem: if a string followed by ''bash>'' appears on the process' output channel within the time bound, the expect command will terminate and succeed.

Another important thing to bear in mind is that processes and pipes are created to implement the communications between the Scala program and the external process. It is mandatory to release these resources at the end of the interaction otherwise your system may run out of resources (e.g. file descriptors). The Expect class provides an destroy method to release the resources. Used together with a proper Automatic-Resource-Management system, e.g. Scala-ARM, it provides a safe way for releasing resources, even when something unexpected happens in the Scala program (e.g. exception).

Using Expect

Add the following dependency in your build.sbt:

libraryDependencies += "org.bitbucket.franck44.expect" %% "expect-for-scala" % "1.0.0"

You may also want to build the current expect-for-scala package (master branch of this repo) and in this case use the command:

sbt publishLocal

This should compile, run the tests and publish the package

Example usage

The simplest usage for the Expect object in the package is to create a process and then use send/expect commands to interact with it. The bash script terminal-sh.sh takes an input of the form cmd1::t1 cmd2::t2 ... cmdk:tk and outputs cmd1 after t1 seconds, cmd2 after t1 + t2 seconds and so on. After the last command cmdk it prints out the prompt "param" on the output stream.

 scala> import org.bitbucket.franck44.expect.Expect
 import org.bitbucket.franck44.expect.Expect

 scala> import scala.util.matching.Regex
 import scala.util.matching.Regex

 scala> import scala.concurrent.duration._
 import scala.concurrent.duration._

 //  create an Expect object with prompt "ready>"
 scala> val e = Expect("src/test/resources/test-terminal.sh", List("ready>"))
 e: org.bitbucket.franck44.expect.Expect = Expect(src/test/resources/test-terminal.sh,List(ready>))

 // expect a prompt ">"
 scala> e.send("read::2\n") ; e.expect(""">""".r, 3.seconds)
 res0: scala.util.Try[String] =
 Success(read
 ready)

A timeout may occur if the process does not send the response within the time bound. Notice that if the process is killed (or crashes) and the input/output streams are closed, Failure may occur.

 scala> import org.bitbucket.franck44.expect.Expect
 import org.bitbucket.franck44.expect.Expect

 scala> val e = Expect("src/test/resources/test-terminal.sh", List("ready>"))
 e: org.bitbucket.franck44.expect.Expect = Expect(src/test/resources/test-terminal.sh, List("ready>"))

 // External process will send "read" after 4 seconds and time bound is 3 seconds
 scala> e.send("read::4\n") ; e.expect("""ready>""".r, 3.seconds)
 res0: scala.util.Try[String] =
 Failure(java.util.concurrent.TimeoutException: Futures timed out after [3 seconds])

 // kill the external process
 scala> e.destroy()

 // Output stream is closed and results in an exception
 scala> e.send("read::4\n") ; e.expect("""ready>""".r, 5.seconds)
 res2: scala.util.Try[String] = Failure(java.util.NoSuchElementException)

If the process generates the answer in successive chunks (with a delay in between the chunks) the expect method collects the characters preceeding the prompt:

 scala> import org.bitbucket.franck44.expect.Expect
 import org.bitbucket.franck44.expect.Expect

 // output "read" after 4 seconds and "write" after 6 seconds
 scala> e.send("read::4 write::2\n") ; e.expect("""ready>""".r, 7.seconds)
 res1: scala.util.Try[String] =
 Success(

 read
 write
 )

In the previous example, ''read'' is generated after 4 seconds and ''write'' 2 seconds after ''read''. The prompt follows immediatly and the full answer "\n\nread\nwrite" is collected (the prompt is not part of the string returned by expect).