Автоматизация работы интерактивных приложений с помощью 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 будет оптимален.
Литература:
- Don Libes Exploring Expect. O'Reilly, 1994. 599 с.
- Кеннет Розен. UNIX System V Release 4. Лори, 1999. 762 с.
- Петерсен Р. LINUX: Полное руководство. 3-е изд. BHV Киев, 2000. 800 c.
- Болл Б. 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/