expect
TIPS
unix系OSでコマンドライン対話を自動化するツール expect
に関するTIPSです。
expect
特有の話と、Tcl の話が混ざってます。
なんとなく、ルーター、L2スイッチ、L3スイッチに対する対話を想定しています。
expect
は Tcl である
expect
のスクリプトは Tcl(ティクル) 言語をベースにしています。
もっと言うと、expect
は Tcl 言語の拡張機能です。
Expect is an extension to the TCL scripting language written by Don Libes.
基本
変数への代入
set MY_VAR "yes"
コマンド展開: []
コマンドを []
で囲うと実行時に展開されます。
if
での判定や、set
での代入にコマンドの実行結果を使いたいときは []
で文を囲います。
# 環境変数 MY_VAR が存在したら何かする
if { [ info exists ::env(MY_VAR) ] } {
# do something
}
# 最初のコマンドライン引数を、MY_VARに代入する
set MY_VAR [lindex $argv 0]
文字列リテラル
$VAR
または ${VAR}
で変数を参照し、展開できます。
文字列リテラルの中でも、[]
はコマンド展開されます。展開されたくないときは \[
、\]
のようにバックスラッシュでエスケープします。
set PROMPT "\[a-zA-Z0-9\._-\]+#"
フロー制御
条件分岐
if { 式1 } {
# 何かする
} elseif { 式2 } {
# 何かする
} else {
# 何かする
}
ループ
for { 初期化文 } { 判定式 } { 繰り返し時に実行する文 } {
# ループで何かする
}
for { set i 0 } { i <= 10 } { incr i } {
# i=0からi=10まで繰り返される
}
ログファイル
通信ログをファイルに記録する
log_file ファイル名
デフォルトだと追記モードです。追記したくない場合は -noappend
を使います。
log_file -noappend ファイル名
ログファイルへの記録を止める
引数無しでコマンド実行すると、ログファイルへの記録が終了します。
log_file
画面出力
ログと標準出力に出力する: send_user
send_user
で標準出力に文字列を出力できます。
log_file
でファイル出力している場合、ログファイルにも記録されます。
改行は自動付与されないので、明示的に書く必要があります。
# 標準出力とログファイルの両方に出力される
send_user "hello\n"
標準出力に出力する: puts
puts
でも標準出力に文字列を出力できます。
log_file
でファイル出力している場合、ログファイルには記録されません。
改行が自動付与されます。
# 標準出力だけに出力される
puts "hello"
実践編
telnet、sshで改行を送信するときは \r
で送る
send "somedata\r"
\n
ではなく \r
です。
コマンドライン引数を取得する
コマンドライン引数は $argv
配列に入っています。
配列は lindex
関数でアクセスします。
https://www.tcl.tk/man/tcl8.5/TclCmd/lindex.htm
$ expect -f myscript.expect arg1 arg2 arg3
if { [ lindex $argv 0 ] == "arg1" } {
send_user "first arg is \"arg1\""
}
spawn
のコマンドラインを表示させない
-noecho
を使います。
spawn ssh 10.0.0.1
# expect の実行時に "spawn ssh 10.0.0.1" と表示される
spawn -noecho ssh 10.0.0.1
# expect の実行時に "spawn ssh 10.0.0.1" が表示されない
環境変数が存在するかチェックする
組み込み関数 info exists
を使ってチェックします。
https://www.tcl.tk/man/tcl8.5/TclCmd/info.htm#M11
if { [ info exists ::env(MY_VAR) ] } {
# do something
}
デバッグする
exp_internal
を使うと expect
の内部状態の遷移が標準出力に出力されます。
exp_internal 1
expect
コマンドで複数のパターンを待つ
expect {
"login:" {
send "$USERNAME\r"
}
"password:" {
send "$PASSWORD\r"
}
}
"login:" が来たら USERNAME変数の値を送信します。
"password:" が来たら PASSWORD変数の値を送信します。
どっちもこなかったら、タイムアウトまで待ってエラーになります。
expect
コマンドで正規表現で待つ
パターンの前に -re
を指定します。
[]
はコマンド展開にならないようにエスケープが必要です。
expect {
-re "(username|login):" {
send "$USERNAME\r"
}
-re "\[Pp\]assword:" {
send "$PASSWORD\r"
}
}
正規表現は tcl の方言
正規表現の .
は改行にはマッチしない
"partial newline-sensitive matching" になっているっぽい。
partial newline-sensitive matching
-
.
は改行文字 (\r
,\n
) にはマッチしない -
^
は入力文字列の先頭にのみマッチする。途中の各行の先頭にはマッチしない。 -
$
は入力文字列の末尾にのみマッチする。途中の各行の末尾にはマッチしない。
正規表現のエスケープは二重エスケープにする
tcl文字列として評価された後に、正規表現としての評価になるので、二重にエスケープが必要です。
expect {
-re "\\S+\[\\r\\n\]" {
# 何かする
}
}
\\S+\[\\r\\n\]
は、tcl文字列として評価すると \S+[\r\n]
になります。これが正規表現として評価されます。
ただし、\r
、\n
については、tcl文字列の評価で 0x0d、0x0a に変換されるので単一エスケープでも動作します。
\\S+\[\r\n\]
は、tcl文字列として評価すると \S+[
0x0d 0x0a ]
になります。
つまり、正規表現エンジンに対して、\r\n
という文字列が与えられるのではなく、0x0d 0x0a という制御文字コードがそのまま与えられることになります。
一方で、\S
や \s
は、tcl文字列としては有効なエスケープシーケンスではないので、単に \
が外されて、S
という文字列になり、S
が正規表現エンジンに与えられます。 アルファベットのS
という文字にしかマッチしません。
行頭にマッチする、の書き方
^
(文字列の先頭) または [\r\n]
(改行文字) を |
(OR条件) で並べて (?:)
(非キャプチャグルーピング) で囲う。
expect {
-re "(?:^|\[\\r\\n\])Password:" {
# 何かする
}
}
または、(?n)
で newline-sensitive matching を指定して、^
にマッチさせる。
接続先のOS依存だが、\r\n
で改行した後に、\r
で行頭にカーソル移動させて表示させていることがあるので、\r?
も入れておくと無難です。
expect {
# 改行後に `\r` が入っている場合でもマッチさせる
-re "(?n)^\\r?Password:" {
# 何かする
}
}
expect
コマンドでマッチした部分を取り出す
$expect_out(0,string)
でマッチした全体を取り出します。
$expect_out(1,string)
で 一つ目の ()
の中身を取り出します。
expect {
-re "(username|login):" {
send "$USERNAME\r"
send_user "prompt is $expect_out(0,string)\n"
# ==> username: (または login:)
send_user "prompt-keyword is $expect_out(1,string)\n"
# ==> username (または login)
}
}