Автоматизация работы интерактивных приложений с помощью Expect

В повседневной работе администратора сети или обычного пользователя значительную часть времени занимает выполнение типовых, однообразных задач. Получение и передача файлов по ftp, сбор статистики с различных устройств, диагностика и администрирование рабочего состояния компьютерных систем. Все эти задачи объединены сходными признаками: они являются интерактивными; число возможных вариантов выполнения задачи ограничено.

Примером такой задачи является копирование файла по ftp. Приведем типичный сценарий получения файла с ftp-сервера (ответы пользователя выделены жирным шрифтом):

$ftp ftp.nsc.rut
220 FTP server ready
Name (ftp.uu.net: user): anonymous
331 Guest login ok, send your complete
 e-mail address as password
Password: user@domen.ru
230 Guest login ok, access restriction apply
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>binary
200 Type set to I.
ftp>cd pub/rf
550 system: No such file or directory
ftp> cd inet/rfc
250 CWD command successful.
ftp> get rfc959.Z
local: rfc959.Z remote: rfc959.Z
200 PORT command successful.
150 Opening BINARY mode data connection
 for rfc959.Z (48587 bytes).
226 Transfer complete.
48587 bytes received in 14.5 secs
 (3.3 Kbytes/sec)
ftp>quit
221 Goodbye.

После завершения этой операции мы получили документ RFC 959, описывающий FILE TRANSFER PROTOCOL (FTP). Операция заняла около минуты. Из полученного документа можно узнать, что при успешном выполнении команды смены каталога ftp-сервер возвращает строку, содержащую код 250. При получении файла код 200 означает, что файл найден и с этого момента начинается его получение. Код 226 означает, что передача файла успешно завершена.

Для автоматизации операции воспользуемся системой Expect1. Рассмотрим сценарий ftp-rfc.exp. У него только одна задача — получить с ftp-сервера файл rfc959.txt.

#!/usr/bin/expect -f
set file "rfc959.txt"
spawn ftp ftp.nsc.ru
expect "Name*:"
send "anonymous
"
expect "Password:"
send "user@domen.ru
"
expect "ftp>"
send "binary
"
expect "ftp>"
send "cd pub/rfc
"
expect "250*ftp>"
send "get $file
"
expect "200*226*ftp>"
send "quit"
close
#-- end of script --

Как видно, в сценарии четыре ключевых момента:

  • spawn - вызов программы ftp-клиента с заданными параметрами (ftp.nsc.ru);
  • expect - ожидание вывода запущенной программой строки, совпадающей с текстовым шаблоном (expect "200
  • 226
  • ftp>");
  • send - передача данных или команд в вызванную программу (send "anonymous ");
  • close - завершение сценария.

Идеология сценария довольно понятна, не правда ли? Система Expect создает для приложения имитацию работы с пользователем — псевдотерминал. По сценарию, определенному пользователем, принимаются данные и передаются команды.

Рассмотрим подробнее, что же представляет собой система Expect.

История и назначение Expect

В сентябре 1987 г. Дон Либес (Don Libes) из Национального института стандартов и технологий (National Institute of Standart and Technology) начал работу над системой Expect, предназначенной для управления интерактивными программами. В настоящее время Expect является расширением языка Tcl, поддерживается языками программирования Си и Perl, добавляя к ним расширенные возможности отладки и удобные команды для описания символьно-ориентированных диалогов.

Необходимо отметить одно важное достоинство системы Expect — единый командный интерфейс. Используя термины и команды Expect, можно построить сценарий управления любой интерактивной программой вне зависимости от ее специфики.

Получение и установка Expect

Дистрибутив последней версии для Unix-систем находится на официальном Web-сервере системы Expect. Для установки системы требуются программные пакеты Tcl (необходим) и Tk (необязателен). Далее:

$tar xvzf expect.tar.gz
$cd expect-X.XX
$./configure && make && /bin/su
#make install

Если установка прошла успешно, двигайтесь дальше. Иначе:

$less INSTALL

Первая программа

Являясь расширением Tcl, Expect наследует и обогащает его лексику. Используя предоставленные возможности, сделаем из приведенного выше сценария нечто более удобное для работы.

#!/usr/bin/expect -f
if $argc!=1 {
send_user "Usage: ftp-rfc.exp
 index_rfc
"
exit
}
set file "rfc$argv.txt"
set timeout 60
spawn ftp ftp.nsc.ru
expect "Name*:"
send "anonymous
"
expect "Password:"
send "user@domen.ru
"
expect "ftp>"
send "binary
"
expect "ftp>"
send "cd pub/rfc
"
expect "550*ftp>" exit
 "250*ftp>"
send "get $file
"
expect "550*ftp>" exit
 "200*226*ftp>"
exec less $file
#-- end of script --

Модифицированный скрипт ftp-rfc.exp, приобретя в качестве аргумента индекс документа RFC, обращается к FTP-серверу ftp.nsc.ru, получает соответствующий документ и выводит его на экран пользователя. Осуществляется проверка кода возврата каждой выполняемой операции. После завершенной неудачно (не найден файл, каталог и т. п.) выполнение скрипта прерывается.

Рассмотрим более подробно основные методы и переменные системы Expect.

Краткий обзор системы Expect

Команда Close — закрыть соединение с текущим процессом

debug [[-now] 0|1]
-now

означает осуществить запуск отладчика Tcl (дебаггера) немедленно — с текущей операции

0 | 1

Остановить (0) или запустить (1) отладчик со следующей операции.

Метод, вызванный без параметров, возвращает 1, если отладчик не запущен, и 0 в противном случае.

Команда Disconnect отсоединяет созданный (forked) процесс от терминала и переводит в фоновый режим. Ввод/вывод процесса перенаправляется в /dev/null. Пример использования:

set delay 3600
send_user "password? "
expect_user -re "(.*)
"
for {} 1 {} {
if [fork]!=0 {sleep $delay;continue}
disconnect
spawn priv_prog
expect Password:
send "$expect_out(1,string)
"
. . .
exit
}

Приведенный сценарий считывает пароль и переходит в фоновый режим. Затем каждый час запускает программу, требующую идентификации по паролю. Таким образом, введя пароль только один раз, можно запускать данную программу бесконечно.

Преимущество использования метода disconnect перед переводом программы в фоновый режим параметром & командной оболочки ($program &) состоит в том, что Expect способен сохранять параметры терминала до отсоединения и затем применить их к создаваемому псевдотерминалу. При выполнении команды программной оболочки & Expect не считывает параметры терминала, так как, когда Expect получит контроль, процесс уже будет от терминала отсоединен.

Метод exit передает системе Expect сигнал подготовиться к завершению работы и затем закончить ее. Этот метод вызывается неявным образом при достижении конца сценария.

expect [[-opts] pat1 body1] ...
 [-opts] patn [bodyn]
-re

Параметр предваряет регулярное выражение.

-ex

Параметр указывает, что необходимо точное соответствие текстовому шаблону. Подстановочные символы «*», «.», «^», используемые в регулярных выражениях, не интерполируются.

-timeout

Переопределяет значение timeout для данного метода.

default

Определяет окончание периода ожидания (timeout) или достижение конца файла/ввода данных (eof).

null

Определяет нулевой (null) символ.

При совпадении выводимых данных с шаблоном выполняются определенные для него операторы. Метод expect возвращает код завершения выполнения операторов или пустую строку, если совпадений с определенными шаблонами не было.

Если с выводимыми данными совпадает несколько шаблонов, выполняются операторы, определенные только для первого.

Для обработки ситуации, когда совпадений с шаблонами нет, необходимо использовать событие timeout (прекращение обработки выводимых вызванной программой данных по истечении времени, определенного в глобальной или локальной переменной timeout).

При определении текстовых шаблонов необходимо учитывать, что строки, начинающиеся с символа «-», зарезервированы для системных целей.

Пример части сценария, проверяющего результат выполнения подключения к удаленному терминалу:

expect {
busy {puts busy
; user_proc}
failed exit
"invalid password" exit
timeout exit
connected
}

От оператора шаблон отделяется пробелом. Для группировки нескольких операторов в один блок используются скобки {} (первый шаблон). Кавычки « » используются для объединения нескольких слов, разделенных пробелами, в один текстовой шаблон (третий шаблон).

Команда fork создает новый процесс — точную копию текущего.

Метод interact передает контроль над текущим процессом пользователю. Введенные команды и данные направляются на вход (stdin) процесса, выходные данные процесса (stdout, stderr) пересылаются пользователю.

Команда log_file [args] [file]

-noappend

означает «стереть содержимое лог-файла перед началом записи». По умолчанию при вызове метода без этого параметра данные просто добавляются в конец лог-файла.

При вызове метода с указанным именем файла в файл начинают записываться результаты выполнения скрипта. При вызове метода log_file без параметров запись результатов в лог-файл прекращается.

Метод send [-flags] string передает строку вызванной программе или процессу.

send_error [-flags] string
Передать строку на stderr
send_log [-] string
Записать строку в лог-файл.
send_tty [-flags] string
Передать строку устройству /dev/tty.
send_user [-flags] string
Передать строку на stdout.

Метод spawn [args] program [args] создает процесс — псевдотерминал, связанный с программой. Переменной spawn_id присваивается значение дескриптора этого процесса. Процесс, дескриптор которого хранится в переменной spawn_id, считается также текущим. Для контроля результатов нескольких одновременно выполняющихся процессов значение переменной spawn_id можно изменять, присваивая ей значения дескрипторов разных процессов.

Переменные

argc, argv, argv0

Параметры вызова программы хранятся в виде списка в переменной argv. Значение переменной argc устанавливается равным числу переданных параметров. Переменная argv0 определена как имя вызванного сценария или бинарного файла.

send_user "$argv0 [lrange $argv 0 $argc]
"
send_slow {interval delay}

Interval — число передаваемых за один раз байт.

Delay — число секунд, разделяющих передачи данных.

Флаг -s переключает метод send в режим замедленной передачи данных, что позволяет избежать обычной ситуации, когда входной буфер программы или устройства, рассчитанный на прием вводимых человеком данных, переполняется при быстром вводе данных из Expect-сценария.

set send_slow {1 0.5}

timeout — глобальная переменная. Определяет, в течение скольких секунд метод Expect ожидает ввода данных, совпадающих с определенными текстовыми шаблонами. Если за данное время совпадение не было обнаружено, выполняются операции, определенные в методе Expect ключевым словом (событием) timeout. В том случае, когда операции, выполняемые по наступлению события timeout, в методе Expect не были определены, то подразумевается выполнение нулевой операции.

По умолчанию timeout присвоено значение в 10 с. Переустановить timeout можно так:

set timeout 60

Неопределенно большая временная задержка может быть установлена значением -1.

Expect как расширение Tcl

Сценарий Expect является и программой на языке Tcl. Как говорилось ранее, система Expect — это программное расширения языка Tcl, т. е. полностью наследуются логика и синтаксис языка. Вот так будет выглядеть определение и вызов подпрограммы в сценарии Expect:

. . .
# Определение подпрограммы, 
осуществляющей смену пароля пользователя
proc update_one_password {user newpassword} {
#вызов команды оболочки passwd
spawn passwd $user
expect "password: "
send $newpasswordк
# подтверждение введенного пароля
expect "password: "
send $newpassword

}
. . .
eval update_one_password $argv
. . .

Следующая часть сценария иллюстрирует применение циклов:

. . .
set default_password new_pass
set list [exec cat list_of_accounts]
foreach account $list {
update_one_password $account
 $default_password
send_user "Пароль для
 пользователя $account изменен."
}
. . .

Следующий пример (e2fsck.exp) является полноценным рабочим сценарием, позволяющим автоматизировать проверку дисков утилитой e2fsck.

#!/usr/bin/expect -f
set timeout -1
spawn /sbin/e2fsck -f $argv
expect {
"Clear? " { send "n" ; exp_continue }
"? "      { send "y" ; exp_continue }
}
Пример использования сценария:
#./e2fsck.exp  /dev/hda3 >log  2>&1

Рассмотрим более интересный сценарий ttrans.exp, осуществляющий передачу файлов любого типа в telnet-сессии. Используются утилиты telnet, uuencode, uudecode, редактор ed.

#!/usr/bin/expect -
if $argc!=1 {
send_user "Usage: ./tr.exp file
"
exit
}
set file [lrange $argv 0 1]
set send_slow {1 0.01}
set timeout 30
set user user_name
set host host_name
set pass user_password
set transf temp_transfer_file
set prompt "(%|#|$)"
catch {set prompt $env(EXPECT_PROMPT)}
spawn telnet $host
expect "ogin"
send "$user
"
expect "password:"
send "$pass
"
expect {
-re $prompt {send_user "
Log in system ok
"}
timeout {send_user "Can't login.
 Exit script.";exit}
}
exec uuencode $file $file > $transf
set out [open $transf r]
send -s "ed -s
"
send -s "a
"
while {-1 != [gets $out Line]} {
send -s $Line
send "
"
send_user "."
}
send ".
"
send "w $transf
"
send "q
"
close $out
send "uudecode $transf 
"
expect -re $prompt
send "rm $transf 
"
exec rm $transf
expect -re $prompt
#interact
send "exit
"
send_user "
Transfer file complete.
"
exit
# -- end of script --

Запуск сценария осуществляется вызовом:

$./ttrans.exp doc.tgz

Поработав со сценарием, можно убедиться, что такой способ копирования файлов по скорости для ftp-протокола не конкурент. Но интересны сама идея возможности передачи данных таким способом и иллюстрируемые этим примером методы Expect.

Expect и Perl

Методы Expect используются в Perl-программах после установки модуля Expect, а также IO::Tty, IO::Stty c CPAN (Comprehensive Perl Archive Network).

Рекомендую последние версии Perl-модулей. Получают список установленных модулей и их версии следующим способом.

При каждой инсталляции Perl-модуля в файл perllocal.pod, расположенный, например, в /usr/lib/ perl5/5.6.0/i386-linux/perllocal.pod, добавляется описание модуля. Просмотреть список можно так:

$ perldoc perllocal

Установка

Для загрузки и установки нового модуля просто наберите в командной строке:

$ perl -MCPAN -e 'install Expect'
Если хотите более детально
 контролировать ход установки, то:
$ tar xvzf Expect-1.15.tar.gz
$ cd Expect-1.15
$ perl Makefile.PL
$ make
$ make test
$ make install

Краткий обзор работы с модулем

Запуск программы осуществляется вызовом функции spawn модуля Expect. В качестве параметров вызова передаются имя программы и аргументы — одной строкой или списком:

my $progr = Expect-> spawn
($program_to_run,@params)
or "Couldn't spawn program:$!";

Функция spawn возвращает объект, представляющий программу, либо, если вызов не удался, undef.

Для уже созданного дескриптора процесса объект Expect инициализируется так:

my $progr = Expect->exp_init
(*FILEHANDLE);

Приверженцы объектно-ориентированного программирования могут написать в следующем виде:

my $exp = new Expect;
$exp->raw_pty(1);
$exp->spawn($command, @parameters)
 || die "Cannot spawn $command: $!
";

$object->debug(0 | 1 | 2 | 3 | undef) устанавливает уровень вывода отладочных сообщений для объекта. Уровень 1 соответствует выводу общей отладочной информации, уровень 2 предоставляет более подробный отчет, уровень 0 отключает вывод отладочных сообщений. Уровень отладки 3 появился в Expect версии 1.05 — вы получаете всю информацию об изменении буфера обмена Expect-объектом. Если вызвать метод без входных параметров (undef), то он вернет текущий уровень отладки. Для вновь созданных объектов уровень отладки устанавливается значением $Expect::Debug.

$object->raw_pty(1 | 0) устанавливает псевдотерминал в raw-режим перед запуском программы, чем отключается эхо-вывод (echo) на STDOUT и CR->LF-трансляцию, а работа с псевдотерминалом становится похожей на работу с процессом. Эта операция должна быть произведена перед вызовом spawn().

$object->expect($timeout, @match_patterns) или, в стиле Tcl/Expect,

expect($timeout,
'-i', [ $obj1, $obj2, ... ],
[ $re_pattern, sub { ...; exp_continue; },
 @subparms, ],
[ 'eof', sub { ... } ],
[ 'timeout', sub { ... }, $subparm1 ],
'-i', [ $objn, ...],
'-ex', $exact_pattern, sub { ... },
$exact_pattern, sub 
{ ...; exp_continue_timeout; },
'-re', $re_pattern, sub { ... },
'-i', @object_list, @pattern_list,
...);

По аналогии с системой Expect для ожидания вывода программой строки используется функция expect. Например, ожидать 50 с до появления приглашения «ftp>»

unless ($progr->expect(50, ?ftp>?))

{ #операция по тайм-ауту}

Ждать неопределенно долго появления «kermit»

unless ($progr->expect
(undef, ?kermit?))

{ #обработка ошибки, произошел сбой в работе программы}

Ждать 50 с до появления приглашения вида «ftp>»или «Ftp>»:

unless ($progr->expect(50,?ftp>?,
 ?Ftp>?)){ # операция по тайм-ауту}

или, используя регулярное выражение с шаблоном поиска /[Ff]tp>]/:

unless ($progr->expect(50,?-re?,?
 [Ff]tp>?)){ # операция по тайм-ауту}

Для использования регулярных выражений при вызове функции expect в качестве первого аргумента указывается время ожидания, в качестве второго — строка «-re», а третий аргумент — строка с шаблоном поиска, затем можно передать другие строки или шаблоны. Возвращаемым скалярным значением Expect является номер элемента, для которого произошло совпадение:

$id = $progr->expect(50, "200",
 "550", "error");
switch ($which)
case 1:	#операция при
 совпадении строки 200
break;
case 2:	#операция при
 совпадении строки 550
break;
case 3:	#операция при
 совпадении строки error
break;

Если ни одна строка не совпала, Expect возвращает false.

Возвращаемым списковым контекстом вызова expect является список из пяти элементов. Первый элемент определяет номер совпавшей строки или шаблона. Второй — строка с описанием причины возврата из expect (при отсутствии ошибок возвращается значение undef, возможные варианты ошибок: «1:TIMEOUT», «2:EOF», «3: spawn id(...) died») и «4:...»(смысл этих сообщений описан в Expect(3)). Третий элемент равен совпавшей строке. Четвертый элемент — текст до совпадения. Пятый — текст после совпадения.

Передать в программу строку «get linux-2.4.17.tgz » можно так:

print $progr ?get linux-2.4.17.tgz
?;

или

$progr->send(?get linux-2.4.17.tgz
?);

Передать управление пользователю:

$progr->interact();

Отключить вывод на STDOUT:

$progr->log_stdout(0);

Если программа завершается сама (например, tail /var/log/messages), то можно использовать:

$progr->soft_close();

Если программа должна быть закрыта извне (например, tail -f /var/log/messages), то принудительное завершение ее:

$progr->hard_close();

Expect::version() — возвращает версию Expect.

$object->log_user(0 | 1 | undef) или
$object->log_stdout(0 | 1 | undef)

разрешает/запрещает отображение переданных объектом данных на STDOUT. Вызов метода без параметров возвращает текущие установки. У созданного объекта данный параметр определяется установленной переменной $Expect:: Log_Stdout variable. Значение по умолчанию 1.

$object->log_file(?filename? | $filehandle | &coderef | undef) — ведение лог-файла. Все символы, переданные или полученные процессом, записываются в файл. Обычно новые данные добавляются в конец уже имеющихся в указанном файле, но, установив режим записи «w», можно в начале сессии очистить содержимое файла:

$object->log_file(?filename?, ?w?);

Вызов метода со значением undef останавливает запись и закрывает лог-файл:

$object->log_file(undef);

При вызове без параметров возвращает имя лог-файла:

$fh = $object->log_file();

Можно указать ссылку на пользовательскую функцию, которая будет вызываться вместо записи в файл:

$object->log_file(&myloggerfunc);
$object->print_log_file(@strings)

— записывает массив @strins в лог-файл или передает для обработки функции, определенной вызовом log_file. Запись текста можно сделать вызовом $object->log_file->print(), но это работает только для лог-файлов, а не для функций обработчиков.

$object->send_slow($delay, @strings);

Метод печатает каждый символ для каждой строки из массива @strings с заданной паузой в $delay секунд. Это удобно для работы с устройствами типа модемов, сбой в функционировании может возникнуть, если передавать данные недостаточно медленно.

Примеры работы с модулем Expect.pm

Использование средств отладки

use Expect;
$Expect::Log_Stdout = 1;
$Expect::Debug = 3;
$Expect::Exp_Internal = 1;
$timeout = 60;
$lpc = Expect->spawn("lpc")
||die "Can't spawn lpc:$!
";
$lpc->Expect($timeout,"lpc>")
 && print $lpc "stat
";
$lpc->hard_close();

Автоматизация подключения к удаленному серверу по ssh

#!/usr/bin/perl
use Expect;
$SSH="/usr/bin/ssh";
if ($#ARGV!=2){
print "usage: ./login-ssh.pl
 host login password
";
exit -1;
}
$timeout=50;
my ($host,$login,$pass)=@ARGV;
my $ssh=new Expect;
$ssh->raw_pty(1);
$ssh->spawn($SSH,"-l", $login,$host)
||die "can't spawn ssh: $!
";
my $spawn_ok;
$ssh->expect($timeout,
[
'assword: $',
sub {
my $fn = shift;
print $fn "$pass
";
exp_continue;
}
],
[
eof =>sub {
if ($spawn_ok){
die "Ошибка: преждевременный выход.
";
} else {
die "Ошибка: некорректная работа с ssh.
";
}
}
],
[
timeout => sub {
die "No login.
"
}
],
"-re","[$#>:]", sub {
my $fn = shift;
$fn->interact();
}
);

Поддержка

Узнать последние новости по разработке и обновлению Perl-модуля Expect, получить ответ на интересующий вас вопрос можно в дискуссионных группах:

http://lists.sourceforge.net/lists/listinfo/expectperl-announce

http://lists.sourceforge.net/lists/listinfo/expectperl-discuss

Ответы на общие вопросы, связанные с Рerl или CPAN, можно найти так:

$ perldoc perlfunc.

Или в Интернете по адресу http://www.perl.com/CPAN-local//.

Expect и C (libexpect)

Пользоваться методами системы Expect в программах на языках Cи и Cи++ поможет библиотека libexpect. В заголовок файла с исходным кодом программы надо включить следующие строки:

#include expect_tcl.h
Expect_Init(interp);

Компиляция такой программы будет выглядеть следующим образом:

$cc myproc.c -o myproc
 -lexpect5.31 -ltcl7.5

Более подробную информацию на эту тему содержит документация, поставляемая с системой.

* * *

Expect предоставляет уникальные возможности для решения системных задач. Являясь общецелевой программной системой, Expect поддерживает работу с сетевыми сервисами и графическим пользовательским интерфейсом. Если для повседневной работы вам нужен только один язык программирования, Expect будет оптимален.

Литература:
  1. Don Libes Exploring Expect. O'Reilly, 1994. 599 с.
  2. Кеннет Розен. UNIX System V Release 4. Лори, 1999. 762 с.
  3. Петерсен Р. LINUX: Полное руководство. 3-е изд. BHV Киев, 2000. 800 c.
  4. Болл Б. Red Hat Linux 7: Энциклопедия пользователя. DiaSoft, 2001. 592 с.

1 Expect (англ.) — ждать, надеяться, предполагать.


Ссылки на проекты и ресурсы

CPAN, http://www.cpan.org

Expect, http://expect.nist.gov

Tcl/Tk, http://www.scriptics.com/

4116