Expect
es hacerlo manualmente
de manera que conozcamos los patrones
de cadenas que intervienen en los ciclos
acción-reacción de la interacción.
El siguiente ejemplo muestra el guión que uso habitualmente
para sincronizar los
ficheros de
estos apuntes
con su
copia
en el servidor ftp del centro en el que trabajo.
Veamos primero el patrón que sigue una conexión convencional.
El servidor requiere una conexión sftp . Después de conectarnos
y proporcionar la palabra clave cambiamos al directorio adecuado
y volcamos los ficheros:
lhp@nereida:~/public_html/perlexamples$ sftp xxxxxxx@ftp.xxxxx.xxx.es Connecting to ftp.xxxxx.xxx.es... xxxxxxx@ftp.xxxxx.xxx.es's password: sftp> cd asignas/asignas/XXX sftp> put index.html Uploading index.html to /var/ftp/pub/asignas/XXX/index.html index.html 100% 23KB 22.8KB/s 00:00 sftp> quit lhp@nereida:~/public_html/perlexamples$Ahora pasamos a automatizarlo:
lhp@nereida:~/Lperl/src/expect/tutorial$ cat -n sftp_put 1 #!/usr/bin/perl -w 2 use strict; 3 use Expect; 4 5 my $host = shift || 'user@ftp.domain.ull.es'; 6 my $localpath = shift || 'local_dir'; 7 my $remotepath = shift || 'remote_dir'; 8 my $glob = shift || '*'; 9 my $command = "put $glob\n"; 10 my $deadline = shift || 8; 11 my $filespercolumn = shift || 4; 12 my $ftp_prompt = 'sftp>\s*'; 13 my $pass_prompt = qr'assword:\s*'; 14 my $password = get_password(); 15 my $ftp; 16 17 $Expect::Log_Stdout = 0; 18 19 sub get_password { 20 my $password = `cat .pass`; 21 chomp($password); 22 $password = reverse $password; 23 "$password\n"; 24 } 25 26 { # Clausura con $count 27 my $count = 1; 28 29 sub upload_handler { 30 my $self = shift; 31 my $a = $self->match(); 32 33 $self->clear_accum(); 34 $a =~ /g +(\S+)/; 35 print "$1\t"; 36 print "\n" if !($count++ % $filespercolumn); 37 exp_continue; 38 } 39 } 40 41 chdir $localpath or die "Cant change to dir $localpath\n"; 42 $ftp = Expect->spawn("sftp", $host); 43 $ftp->expect( 44 $deadline, 45 [$pass_prompt, sub { print $ftp $password; } ], 46 $ftp_prompt 47 ); 48 49 my $err = $ftp->error(); 50 die "Deadline $deadline passed. $err\n" if ($err); 51 print "Successfully connected to $host\n"; 52 53 print $ftp "cd $remotepath\r"; 54 $ftp->expect($deadline, '-re', $ftp_prompt) 55 or die "After <cd>. Never got prompt ".$ftp->exp_error()."\n"; 56 print "Changing directory to $remotepath\n"; 57 58 print "Executing $command ...\n"; 59 print $ftp $command; 60 $ftp->expect( 61 $deadline, 62 [qr'Uploading +\S+', \&upload_handler], 63 $ftp_prompt 64 ); 65 $err = $ftp->error(); 66 die "Deadline $deadline passed. $err\n" if ($err); 67 print("\nAfter $command\n"); 68 69 print $ftp "quit\r"; 70 $ftp->hard_close();
Enviar entrada a un programa controlado con Expect
es
tan sencillo como hacer un print
del comando.
Un problema es que terminales, dispositivos y sockets
suelen diferir en lo que esperan que sea un terminador de línea.
En palabras de Christiansen y Torkington [3]
We've left the sanctuary of the C standard I/O libraryy la conversión automática de
\n
a lo que corresponda
no tiene lugar. Por eso verás que en algunas expresiones regulares
que siguen se pone \r
o cualquier combinación que se
vea que funciona.
En la línea 42 se llama a spawn como método de la clase:
$ftp = Expect->spawn("sftp", $host)
esta llamada produce un fork y se ejecuta (exec) el comando
sftp
. Retorna un objeto Expect
, esto es
un objeto que representa la conexión con el programa
lanzado y que contiene la información
sobre la Pseudo terminal TTY (Véase la clase IO::Pty ).
El truco en el que se basa Expect
es este: hacerle creer
al proceso al otro lado que está en una sesión interactiva con
una terminal. De ahí que el objeto sea una terminal virtual.
Si spawn
falla, esto es, si el programa
no puede arrancarse, se retorna undef
. La sintáxis
mas usada es:
Expect->spawn($command, @parameters)
Los parámetros se le pasan sin cambios a exec
.
Si el comando no se pudo ejecutar el objeto Expect
sigue siendo válido y la siguiente llamada a expect
verá un "Cannot exec"
. En este caso el objeto se ha creado
usando spawn
pero podría haberse creado primero con
el constructor new
.
El método new admite dos formas de llamada:
$object = new Expect ()
$object = new Expect ($command, @parameters)
spawn
.
Es posible cambiar los parámetros del objeto antes de hacer
spawn
. Esto puede ser interesante si se quieren cambiar
las características de la terminal cliente.
Por ejemplo, antes de ejecutar el spawn podemos llamar
al método $object->raw_pty(1) el cual desactivará el
eco y la traducción de
produciendo una conducta mas parecida
a la de un pipe Unix.
El patrón del programa es una iteración de unidades
de acción-reacción. Por ejemplo, en la línea 53
emitimos el comando para cambiar a un determinado directorio
de la máquina remota mediante Expect.
Los métodos print
y send
son sinónimos.
Así la línea 53 puede ser reeescrita como:
ftp->send("cd $remotepath\r");
La respuesta se recibe mediante el método expect.
El esquema de funcionamiento de expect
es que tenemos un
manejador de fichero/flujo abierto desde
un proceso interactivo y le damos tiempo suficiente
hasta que la cadena que se va recibiendo case con una expresión
regular que proporcionamos como límite.
Una vez que el buffer casa con la expresión regular se libera
todo el texto hasta el final del emparejamiento.
La forma mas simple de llamada a expect
sigue el formato:
$exp->expect($timeout, '-re', $regexp);
o un poco mas general
$exp->expect($timeout, @match_patterns);
El parámetro $timeout
dice cuantos segundos se esperará
para que el objeto produzca una respuesta que case con una
de los patrones. Si $timeout
es undef
se esperará
un tiempo indefinido hasta que aparezca una respuesta que case
con uno de los patrones en @match_patterns
.
Es posible poner varias expresiones regulares en la llamada:
$which = $object->expect(15, 'respuesta', '-re', 'Salir.?\s+', 'boom'); if ($which) { # Se encontró una de esos patrones ... }
En un contexto escalar expect
retorna el número
del argumento con el que ha casado: 1 si fué respuesta
,
2 si casó con Salir.?\s+
, etc. Si ninguno casa se retorna
falso.
Una segunda forma de llamada a expect
puede verse en las
líneas 43 y 60. Estas dos llamadas siguen el formato:
$exp->expect($timeout, [ qr/pattern1/i, sub { my $self = shift; .... exp_continue; }], [ qr/pattern2/i, sub { my $self = shift; .... exp_continue; }], ... $prompt );En este caso
expect
establece un bucle.
Si se casa con pattern1
se ejecuta la subrutina apuntada
por la referencia asociada. Si esta termina en exp_continue
se sigue en expect
. En ese caso el cronómetro se arranca de nuevo
y el tiempo se empieza a contar desde cero.
Si se casa con pattern2
se ejecuta la segunda subrutina, etc.
Si se casa con $prompt
o se sobrepasa el
límite $timeout
se sale del lazo.
Por ejemplo, en la línea 58:
58 $ftp->expect( 59 $deadline, 60 [qr'Uploading +\S+', \&upload_handler], 61 $ftp_prompt 62 );
Después del put
, el mensaje Uploading file
sale
por cada fichero que se transfiere. Eso significa que el manejador
upload_handler
se estará ejecutando hasta
que aparezca de nuevo el prompt o bien se supere
el tiempo limite establecido.
El manejador upload_handler
(líneas 29-38)
obtiene mediante la llamada al método
match
la cadena que casó con la expresión regular
que ha triunfado Uploading +\S+
.
El método clear_accum
limpia el acumulador
del objeto, de manera que la próxima llamada a
expect
sólo verá datos nuevos.
Mediante la expresión regular de la línea 34
se detecta el nombre del fichero implicado
y se muestra por pantalla.
Para cerrar la sesión (línea 70) llamamos al método hard_close .
Una alternativa a este es usar soft_close o hacer
una llamada a expect
esperando por 'eof'
.
Sin embargo esta alternativa toma mas tiempo. En cualquier caso
es importante asegurarnos que cerramos adecuadamente y
no dejamos procesos consumiendo recursos del sistema.
Al ejecutar el programa anterior obtenemos:
lhp@nereida:~/Lperl/src/expect/tutorial$ sftp_put Successfully connected to username@ftp.domain.ull.es Changing directory to remote_dir Executing put * .......... .......... .......... .......... images.aux.gz images.bbl images.bbl.gz images.idx images.idx.gz images.log images.log.gz images.out images.pl images.pl.gz images.tex images.tex.gz img1.old img1.png img10.old img10.png img10.png.gz img100.old img100.png img101.old img101.png img102.old img102.png img103.old .......... .......... .......... .......... node95.html.gz node96.html node96.html.gz node97.html node97.html.gz node98.html node98.html.gz node99.html node99.html.gz pause2.png perl_errata_form.html perlexamples.css .......... .......... .......... .......... After put *Veamos una segunda ejecución a otra máquina que no solicita clave (conexión via
ssh
usando clave pública-clave privada):
lhp@nereida:~/Lperl/src/expect/tutorial$ sftp_put europa '.' '.' etsii Successfully connected to europa Changing directory to . Executing put etsii ... etsii After put etsii
expect
de las líneas 43-47?
La variable $Expect::Log_Stdout
(línea 17) controla la salida de los
mensajes de log por STDOUT
. Normalmente vale 1. Tiene asociado
un método get/set $object->log_stdout(0|1|undef)
(alias
$object->log_user(0|1|undef)
). Cuando el método se llama sin parámetros
retorna los valores actuales. Al poner la variable a cero
$Expect::Log_Stdout = 0;desactivamos globalmente la salida.
Casiano Rodríguez León