The expect C library is an altenative to using plain expect scripts, specially useful if you are a C or C++ programmer who also mange Linux hosts. The program that follows was built in Debian, it is a simple demonstration on how to use the library.
Let us start by installing the library and other tools we need.
apt-get update
apt-get install build-essential libgoogle-glog-dev expect-dev tcl8.5 dev
The function below executes a single command and exit after successful ssh login. Please refer to the inline comments and the description that follows for guidance.
#include <sys/wait.h>
#include <tcl8.5/expect.h>
#include <glog/logging.h>
#include <iostream>
#include <fstream>
#include <string>
#include <iterator>
#include <sstream>
#include <vector>
using namespace std;
int task(const string& host, const string& password,
const string& command) {
exp_is_debugging = 0;
exp_timeout = 20;
// exp_open spawn our ssh/program and returns a stream.
FILE *expect = exp_popen((char *) host.c_str());
if (expect == 0) {
return 1;
}
enum { denied, invalid, command_not_found,
command_failed, prompt };
// exp_fexpectl takes variable parameters terminated by
// exp_end. A parameter takes the form
// {type, pattern, and return value} corresponding to
// exp_glob, "password", prompt, in exp_fexpectl call below.
// If a pattern matches, exp_fexpectl returns with the value
// corresponding to the pattern.
switch (exp_fexpectl(expect,
exp_glob, "password: ", prompt,
exp_end)) {
case prompt:
// continue
break;
case EXP_TIMEOUT:
LOG(ERROR) << "Timeout, may be invalid host" << endl;
return 1;
}
bool success = true
// In sending command the return character \r is required.
fprintf(expect, "%s\r", password.c_str());
switch (exp_fexpectl(expect,
exp_glob, "denied", denied, // 1 case
exp_glob, "invalid password", invalid, // another case
exp_glob, "$ ", prompt, // third case
exp_end)) {
case denied:
LOG(ERROR) << "Access denied" << endl;
success = false;
break;
case invalid:
success = false;
LOG(ERROR) << "Invalid password" << endl;
break;
case EXP_TIMEOUT:
LOG(ERROR) << "Login timeout" << endl;
break;
case prompt:
// continue;
break;
default:
break;
}
if (!success) {
fclose(expect);
waitpid(exp_pid, 0, 0);
return 1;
}
// Note that the pattern used below are dependent
// on the command. Adding more cases to cover
// various command will help.
fprintf(expect, "%s\r", command.c_str());
switch (exp_fexpectl(expect,
exp_glob, "cannot create", command_failed,
exp_glob, "command not found", command_not_found,
exp_glob, "$ ", prompt,
exp_end)) {
case command_failed:
LOG(ERROR) << "Could not create directory" << endl;
success = false;;
break;
case command_not_found:
LOG(ERROR) << "Command not found" << endl;
success = false;
break;
case prompt:
fprintf(expect, "%s\r", "exit");
LOG(INFO) << "Task completed." << endl;
break;
}
// You should always do this.
// exp_popen spawns a process with pid exp_pid and
// return a FILE object.
fclose(expect);
waitpid(exp_pid, 0, 0);
if (success)
return 0;
else
return 1;
}
Note that exp_fexpectl will not return and will wait for the pattern to arrive until exp_timeout. Make sure you have all the expected string patterns covered.
The pattern type exp_glob is an enum which determines how the pattern is interpreted. Use exp_regex if you want to use reqular expressions in the case string pattern. Please see expect.h in your include directory for more options.
exp_fexpectv can be used instead of exp_fexpectl. exp_fexpectv takes struct exp_case as parameter. The code fragment below corresponds to the section of code above beginning at line 52.
struct exp_case cases [] = {
{ "denied", NULL, exp_glob, denied},
{ "invalid password", NULL,exp_glob,invalid},
{ "$ ", NULL, exp_glob, prompt},
0
};
switch (exp_fexpectv (expect, cases)) {
case denied:
LOG(ERROR) << "Access denied" << endl;
retval=error;
break;
case invalid:
retval=failure;
LOG(ERROR) << "Invalid password" << endl;
break;
case prompt:
// continue
break;
case EXP_TIMEOUT:
LOG(ERROR) << "Login timeout" << endl;
break;
}
The terminating null character in exp_case cases above is required.
We have 3 switch blocks above which makes the code a little bit longer. If preferred, all the 3 switch blocks can be placed in a loop and then expand the case patters to cover all possible prompts.
The main function.
int main(int argc, char *argv[]) {
// Initialize logging
google::InitGoogleLogging("exp");
ifstream tasks("tasks.txt", ifstream::in);
// This is how we read our tasks.txt file. You may
// want to employ a way you are comfortable with.
if (tasks.is_open()) {
for (string line; getline(tasks, line);) {
stringstream str(line);
istream_iterator<string> it(str);
istream_iterator<string> end;
vector<string> result(it, end);
string password;
ostringstream command, host;
host << "ssh ";
for (vector<int>::size_type i = 0;
i != result.size(); i++) {
switch (i) {
case 0:
host << result [i];
break;
case 1:
password = result [i];
break;
default:
command << result [i] << " ";
break;
}
}
if (task(host.str(), password, command.str()) == 1) {
// Error occurred. You may want to stop
// at this point if the next task is dependent
// on the previous task.
}
}
}
return 0;
}
Our programs takes a text file, tasks.txt as input. Each line is separated by spaces with the first to being use@host and password. The third and beyond is the command and its parameters.
To build the program above use the following from your command line.
g++ exp.cpp -Wall -std=c++0x -lexpect -ltcl8.5 -lglog -o exp
That's it Good Luck.