expect
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 |