expect

Aus Mikiwiki
Zur Navigation springen Zur Suche springen

Die Skriptsprache expect dient zur Automatisierung von interaktiven Aufgaben unter Unix. Es handelt sich um eine Erweiterung der Skriptsprache TCL für interaktive Anwendungen wie telnet, ftp, passwd, fsck, rlogin, ssh und andere. Mit Tk können interaktive Anwendungen auch in X11-Benutzeroberflächen eingepackt werden.

expectk besteht aus expect und Tk; letzteres ist ebenso wie expect eine TCL-Erweiterung zur Gestaltung grafischer X-Oberflächen.

Installation

Debian GNU/Linux 10

$ sudo apt-get install expect
Paketlisten werden gelesen... Fertig
Abhängigkeitsbaum wird aufgebaut.       
Statusinformationen werden eingelesen.... Fertig
Die folgenden zusätzlichen Pakete werden installiert:
  libtcl8.6 tcl-expect tcl8.6
Vorgeschlagene Pakete:
  tk8.6 tcl-tclreadline
Die folgenden NEUEN Pakete werden installiert:
  expect libtcl8.6 tcl-expect tcl8.6
0 aktualisiert, 4 neu installiert, 0 zu entfernen und 0 nicht aktualisiert.
Es müssen 1'426 kB an Archiven heruntergeladen werden.
Nach dieser Operation werden 4'989 kB Plattenplatz zusätzlich benutzt.

Verwendung

Beispiel 1

Folgendes Skript startet passwd, um das Passwort für den Benutzer "abc" einzustellen. Das Programm wird mit dem Befehl "spawn" gestartet, der dieses dann in einem neuen Prozess ablaufen lässt. Danach kommt der magische Befehl "expect", mit dem Argument "word:". Das Skript wartet, bis dieses Zeichenmuster vom mit "spawn" erzeugten Prozess (hier also passwd) ausgegeben wird. Danach wird der Befehl "send" benutzt, um etwas an den Prozess zu schicken; dies entspricht dann einer normalen Eingabe über die Tastatur. Weil ein Schreiben zu "passwd" immer mit "Return" bestätigt werden muss, muss an den Befehl "\r" (carriage return) angehängt werden. Dieses Senden entspricht der "Reaktion" im Chatfile. Diese Befehlsfolge stellt die erste Passwortabfrage von passwd dar. Danach folgt eine Bestätigung, die sich in den folgenden zwei Zeilen widerspiegelt. Das letzte "expect" wartet noch, bis passwd seine Ausgabe vollständig beendet hat.

spawn passwd abc
expect "word:"
send "cool\r"
expect "word:"
send "cool\r"
expect eof

Dieses einfache Script kann über folgende Anweisung aufgerufen werden:

$ expect scriptfile

Das Skript kann auch gestartet werden, indem als erste Zeile "#!/usr/bin/expect" eingefügt und das Skript dann mit chmod ausführbar gemacht wird.

Damit das Skript nicht jedesmal angepasst werden muss, können Befehlszeilenparameter verwendet werden:

spawn passwd [lindex $argv 0]
set password [lindex $argv 1]
expect "word:"
send "$password\r"
expect "word:"
send "$password\r"
expect eof

Der folgende Aufruf wird das expect-Skript "scriptfile" aufrufen und das Passwort des Benutzers "abc" (erstes Argument) auf "cool" (zweites Argument) setzen:

$ expect scriptfile abc cool

Um auf das n-te Befehlszeilenargument zugreifen zu können, wird folgender Befehl verwendet:

set variablen-name [lindex $argv n-1]

Dabei ist zu beachten, dass bei "set" kein $-Zeichen vor den Variablennamen gesetzt wird, bei einem späteren Zugriff wird es jedoch benötigt (dies ist eine Eigenschaft von TCL, worauf expect ja beruht).

Beispiel 2

Das von expect erwartete Suchmuster kann auch viel grosszügiger und flexibler bestimmt werden:

send "Bitte Namen eingeben :\n"

expect "Fritz" {send "Sei mir gegrüsst\n"}\
       "Franz" {send "Hi, wie geht's?\n"}\
       "Keith" {send "Hallo Gitarrengott\n"}

Es ist also grundsätzlich nicht notwendig, mit "spawn" zuerst ein interaktives Programm zu starten. Denn "send" schreibt hier einfach auf stdout und expect liest von stdin, sodass darüber auch solche Abfragen verwirklicht werden können. Solange kein Prozess durch "spawn" initialisiert wurde, zeigt expect dieses Verhalten.

Das Besondere an diesem Skript ist aber der expect-Block, der aus einem bestimmten Muster sowie einem zugeordneten "send" besteht. Die oben gezeigten Paare sind auch ohne den gesamten Block möglich, so dass Konstrukte wie das folgende oft benutzt werden:

expect "Tom" {send "Cooler Kerl!\n"}

Beispiel 3

Anstatt festgelegter Zeichenketten, können auch Wildcards und ähnliches benutzt werden.

Der expect-Befehl im folgenden Beispiel beinhaltet "*" als Wildcard, sodass nur dann fortgefahren wird, wenn "220" am Anfang der Zeichenkette und "ready." an ihrem Ende stehen. Neben "*" kann auch "?" (für ein einzelnes Zeichen) benutzt werden.

spawn ftp 127.0.0.1
expect "220*ready."
send "FTP Server ist bereit\n"

So können auch bestimmte Zeichen herausgefiltert und auf deren Existenz geprüft werden. Es ist auch möglich "[abcd]" anzugeben, wodurch auf eine Zeichenkette gewartet wird, die nur diese Buchstaben enthält.

send "Bitte geben Sie etwas ein:\n"
expect "\[0-9]" {send "Sie haben eine Zahl eingegeben!\n"}

spawn

Der Befehl "spawn" startet einen Prozess, auf den dann mit "send" und "expect" zugegriffen werden kann.

# dieser erste Aufruf legt die ID für ftp in $spawn_id ab
spawn ftp 127.0.0.1

# $spawn_id für ftp in $ftp_spawn_id sichern
set ftp_spawn_id $spawn_id

# dieser zweite Aufruf legt die ID für telnet in $spawn_id ab
spawn telnet 127.0.0.1

# $spawn_id für telnet in $telnet_spawn_id sichern
set telnet_spawn_id $spawn_id

# wir wollen mit ftp reden, weshalb $spawn_id wieder auf ftp ID gesetzt wird
set spawn_id $ftp_spawn_id

send "Dieser Text wird an ftp-Prozess gesendet!"

# wir wollen mit telnet reden, weshalb $spawn_id wieder auf telnet ID gesetzt wird
set spawn_id $telnet_spawn_id

send "Dieser Text wird an telnet-Prozess gesendet!"

Mit zweimaligem Aufruf von "spawn" werden zwei Prozesse gestartet (ftp, telnet).

Jeder mit "spawn" gestartete Prozess bekommt eine ID, die bei einem Aufruf von "spawn" in der globalen Variable "$spawn_id" gespeichert. Wird "spawn" ein zweites Mal aufgerufen, so wird die ID dieses neuen Prozesses in "$spawn_id abgelegt. Diese Variable legt den Prozess fest auf den sich die expect-Befehle (expect, send, usw.) beziehen. Deshalb ist es möglich durch entsprechende Veränderung der Variable den Prozess auszuwählen, mit dem expect kommunizieren soll. So kann man also eine Art Multi-Processing erreichen. Leider ist das häufige Setzen von $spawn_id mit "set spawn_id ..." recht aufwendig; es gibt aber auch einen einfacheren Weg. Anstatt

set spawn_id $telnet_spawn_id
send "Dieser Text wird an telnet-Prozess gesendet!"

kann auch folgendes geschrieben werden:

send -i $telnet_spawn_id "Dieser Text wird an telnet-Prozess gesendet!"

Diese Schreibweise umgeht die globale Variable "$spawn_id" und benutzt einfach die ID, die hinter dem Parameter "-i" angegeben wird.

Kontrolle der Ausgabe

Um die Ausgabe (bzw. der ablaufende Dilaog) der durch "spawn" gestarteten Prozesse zu unterbinden, kann folgender Befehl verwendete werden:

log_user 0

Um die Ausgabe wieder sichtbar zu machen:

log_user 1

Viele Anwendungen werden wohl nie ganz ohne Ausgabe auskommen. Im folgenden Beispiel wird die Zeichenkette nicht zum entsprechenden Prozess sondern auf stdout ganz normal ausgegeben, so dass man ihn lesen kann:

send_user "Text"

Erstellung von Protokolldateien

Anstatt die Dialogausgabe auf stdout auszuschalten, kann sie auch in eine Datei umgeleitet werden. Allerdings nicht durch den Aufruf mit ">", sondern mit folgendem in expect eingebauten Befehl, wobei "filename" die Datei bezeichnet, die als Protokolldatei benutzt werden soll.

log_file flag filename
-a Alle Ausgaben sichern. "log_file" zeichnet normalerweise nur das auf, was der Benutzer auf stdout auch zu sehen bekommt. Womöglich möchte man als Benutzer keine Ausgabe sehen und hat diese mit "log_user 0" deaktiviert. Trotzdem soll vielleicht der Dialog aufgezeichnet werden, was aber aufgrund des Ausschaltens der Ausgabe auf stdout ja nun nicht mehr möglich ist. Der Parameter "-a" zeichnet selbst in dieser Situation noch alles auf, denn er ist unabhängig von der Ausgabe auf stdout.
-noappend Kein Anfügen an eventuell schon vorhandenen Dateiinhalt. Bereits vorhandene Dateien werden also überschrieben! Ist keine Datei vorhanden, so wird sie angelegt.

Um die Aufzeichnung an einer bestimmten Stelle im Skript zu unterbrechen oder zu beenden, reicht folgender Aufruf:

log_file

Ein erneuter Beginn der Aufzeichnung kann dann wie folgt erreicht werden, wobei natürlich beachtet werden sollte, dass die Verwendung von "-noappend" die zuvor aufgezeichneten Daten löschen würde, falls derselbe Protokolldateiname verwendet wird.

log_file flag filename

Timeouts

expect wartet solange, bis ein Befehl das Muster in der Ausgabe des Prozesses gefunden hat. Wird innerhalb einer bestimmten Zeitspanne keine Ausgabe gefunden, die zum angegebenen Muster passt, kommt es zu einem Timeout; expect bricht dann das Warten ab und fährt fort. Die Timeout-Zeit liegt standardmässig bei 10 Sekunden.

Folgende Anweisung setzt die globale Variable "$timeout", welche die Timeout-Zeit in Sekunden festlegt.

set timeout 60

Um ganz auf das Warten auf ein Timeout zu verzichten, kann einfach 0 angegeben werden. Um expect dazu zu zwingen, auf die Zeichenkette zu warten, ohne vorher von einem Timeout-Signal unterbrochen zu werden, wird "-1" angegeben, was für eine unendlich lange Timeout-Zeit steht.

expect_out

Wenn expect zurückkehrt (also das Muster gepasst hat), liefert es die gesamte Zeichenkette, die das angegebene Muster erfüllte gleich in einer Variable mit.

Beispielskript:

send "Bitte geben Sie einen Satz ein :\n"
expect "Linux"
send "Sie gaben folgenden Satz ein : \n$expect_out(buffer)\n"

$ expect script.exp
Bitte geben Sie einen Satz ein :
Dieses Magazin kämpft für Linux
Sie gaben folgenden Satz ein :
Dieses Magazin kämpft für Linux

Fehlerbehandlung

expect schreibt seine Ausgabe nach "stdout". Um beispielsweise Fehlermeldungen nach "stderr" schreiben zu können (damit sie sich von anderen Meldungen unterscheiden), kann der Befehl "send_error" verwendet werden.

Im folgenden Skript wird mit Hilfe einer TCL "if"-Kontrollstruktur die Anzahl der Befehlszeilenparameter überprüft. Ist diese ungleich 2, wird der Befehl "send_error" benutzt, um die Usage-Anweisung auszugeben. Das anschliessende "exit" terminiert die Anwendung mit dem Rückgabecode "1" (Fehler). Stimmte die Anzahl der Parameter (nämlich 2), so trifft die Bedingung nicht zu, und das Programm gibt mit "send" einfach die Zeichenkette "Der Aufruf war korrekt" auf "stdout" aus.

if {$argc!=2} {
  send_error "Usage : $argv0 Param1 Param2\n"
exit 1;
}
send "Der Aufruf war korrekt\n"

interact

Um an einem bestimmten Punkt des Skriptes wieder selber mit dem Programm zu interagieren, kann der Befehl "interact" verwendet werden.

Im folgenden Skript wird eine telnet-Sitzung zum Rechner "localhost" geöffnet, wobei die Automatisierung das vollständige Login übernimmt. Danach wird die Kontrolle mit "interact" wieder an den Benutzer übergeben. Expect bleibt danach solange still, bis der durch "spawn" erzeugte Prozess (hier die telnet-Sitzung) beendet wird, dann übernimmt expect wieder die Kontrolle.

spawn telnet 127.0.0.1
expect "ogin:"
send "abc\r"
expect "assword:"
send "secret\r"
interact

Beispiel: FTP-Automatisierung

Ein FTP-Server bietet hunderte von Dateien an, die man herunterladen möchte. Das Benennungsschema dieser Dateien besteht aus einem konstanten Teil und einer fortlaufenden Nummer (bestes Beispiel RFC-Dateien: rfc2166.txt usw.). Wie kommt man nun am schnellsten an diese Dateien heran? Das Problem beim Lösen einer solchen Aufgabe z. B. unter Perl ist das Verskripten der Interaktivität (z. B. das Login) von ftp.

#Script : rfcgrab
set host   [lindex $argv 0]
set path   [lindex $argv 1]
set pstart [lindex $argv 2]
set pend   [lindex $argv 3]
set start  [lindex $argv 4]
set end    [lindex $argv 5]

#Usage-Check
if {$argc!=6} {
  send_error "Usage : $argv0 Host Path-to-files Prefix Suffix start# end#\n"
  exit 1;
}

# Warten bis die Aktion ausgeführt wurde
set timeout -1

# ftp starten
spawn ftp $host 

# Gast-Login
expect "):"
send "anonymous\r"

# E-Mail als Passwort
expect "assword:"
send "wartron@yahoo.com\r"

# Wechsel ins Verzeichnis, wo sich die gewünschten Dateien befinden
expect ">"
send "cd $path\r"

# Herunterladen jeder einzelnen Datei (die for-Schleife gehört zu TCL)
for {set index $start} {$index <= $end} {incr index} {
  expect ">"
  send "get\r"

  expect ")"
  send "$pstart$index$pend\r"

  expect ")"
  send "$pstart$index$pend\r"
}

Aufruf des Skripts "rfcgrab", beispielsweise um die RFCs von 2000 bis 2100 abzuholen. Dabei werden auf dem Rechner "ftp.internic.net" im Verzeichnis "/rfc" alle Dateien abgeholt, die mit "rfc" beginnen und auf ".txt" enden:

$ expect rfcgrab ftp.internic.net /rfc/ rfc .txt 2000 2100

Beispiel: SSH-Anmeldung

#!/usr/bin/expect -f
# Establishes an ssh connection for user "root" on host "abc"
spawn ssh root@abc
expect "password:"
send "xxxxxx\r"
expect "root@abc:~# "
send "ls -al\r"
interact

Weblinks

Herausgeber Sprache Webseitentitel Anmerkungen
National Institute of Standards and Technology eng The expect home pagewbm
Offizielle Homepage. - Autor: Don Libes
Wikipedia eng Expectwbm Enzyklopädischer Artikel
Tcl.Tk eng Expectwbm
Linux Magazin ger Schwere Arbeit leicht gemachtwbm Autor: Mark Vogelsberger
Pro-Linux ger Konsolenprogramm mit expect fernsteuernwbm
Megalinux eng Expect/Bash script with timeoutwbm