Expectについて
ExpectはLinuxのCLIコマンドを自動化する化するパッケージの一つです。
普通のbashでも自動化できるじゃないですか。というご意見もあると思います。
実際間違ってはいません。対話型に特化しているツールとおもっていただきたいです。
公式ドキュメント
日本語ドキュメント <--ここがわかりやすいです。
bashの簡単なおさらい
bashのおさらい
#!/bin/bash
ls /var
# result
# backups lib lock mail run spool tmp
# cache local log opt snap swap www
変数の扱いについて
#!/bin/bash
test1="varchar"
test2="date"
test3=`date`
test4[0]=0
test4[1]=1
test4[2]=2
test4[3]=3
echo test1=${test1}
echo test2=${test2}
echo test3=${test3}
echo test4[0]=${test4[0]}
echo test4[1]=${test4[1]}
echo test4[2]=${test4[2]}
echo test4=${test4}
echo test4[@]=${test4[@]}
# result
# test1=varchar
# test2=date
# test3=2021年 4月18日 日曜日 21時15分20秒 JST
# test4[0]=0
# test4[1]=1
# test4[2]=2
# test4=0
# test4[@]=0 1 2 3
#!/bin/bash
for i in {1..10} ; do
echo ${i}
done
# result
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
# 10
そのほかfor文
#!/bin/bash
test4[0]=0
test4[1]=1
test4[2]=2
test4[3]=3
for i in ${test4[@]} ; do
echo ${i}
done
# result
# 0
# 1
# 2
# 3
#!/bin/bash
# Check created test directory
if [ -d "test" ]
then
echo "Directory test exists."
else
echo "Error: Directory test does not exists."
fi
# result
# Directory test exists.
オプション | 説明 |
---|---|
-d | ディレクトリなら真 |
-f | 普通のファイルなら真 |
-s | サイズが 0 より大きければ真 |
-e | 存在するなら真 |
-r | 読み取り可能なら真 |
-w | 書き込み可能なら真 |
-x | 実行可能なら真 |
オプション | 説明 | 補足 |
---|---|---|
-eq | 等しければ真 | equal |
-ne | 等しくなければ真 | not equal |
-lt | より小なら真 | less than |
-le | 以下なら真 | less than or equal |
-gt | より大なら真 | greater than |
-ge | 以上なら真 | greater than or equal |
コマンド実行、if文にfor文この時点で色々なことができるでしょう。
次の例ではどうでしょうか。
お題1
sshでAサーバにログインしてください。
以下、ローカル「192..168.11.50」のラズパイサーバがあるとしましょう。
#!/bin bash
ssh pi@192.168.11.50
# pi@192.168.11.48's password: <--入力を求められてしまう。
これでは自動化は難しいですね...
何百台とある場合の手作業は大変なものです。
あーなんとかして入力待ちを検知して、自動で入力してくれないものか...
ここで便利なのがexpectです。
#!/bin/bash
expect -c "
set timeout 3
spawn ssh pi@192.168.11.50
expect \"assword:\"
send \"raspberry\n\"
interact
"
上記のように書くことで、入力待ちに対して、自動で入力が可能となります。
bashからexpect読み込んでいますが、次のようにも書くことが可能です。
好みの問題かと思いますので、自由に選んでください!
一番先頭の**#!/bin/bash** や #!/usr/bin/expect -f
の意味は、次を参考に学んでみてください。
参考URL
bashではなく、expectとして同じものを書いてみましょう。
#!/usr/bin/expect -f
set timeout 3
spawn ssh pi@192.168.11.50
expect "assword:"
send "raspberry\n"
interact
"" で囲わなくていいのでこちらのほうが楽かもしれませんね!
個人的には、expectで書く方法をおすすめしています。
Expect のインストール
CentOS7
$ yum install expect
mac
# brew install expect
Expectの基礎構文
よく使うであろう、基礎構文について学んでみましょう!
set
setは、変数を宣言するときに利用します。
dataにhelloという文字列をいれてみましょう。
#!/usr/bin/expect -f
set data "hello"
puts $data
# result
# hello
set timeout
timeoutには、特集な意味を持つ変数です。
expectでbashからレスポンスを待つ時間を設定できます。
項目 | 値、設定方法 |
---|---|
default | 10 second |
1秒に変更 | set timeout 1 |
30秒に変更 | set timeout 30 |
無制限 | set timeout -1 |
初めに、timeoutが起こるプログラムを作成しましょう。
みなさまの家庭には一家に3台はあるとい言われているrassberry piですが、我が家にはrassberry pi を1つしかありませんでした
#!/bin/bash
word=("wait" "run" "eof")
for i in {1..5} ; do
j=0
if [ ${j} -eq 0 ]; then
if [ ${word[${j}] } = "wait" ]; then
sleep 10
read -p "wait:" input_data
if [ ${input_data} = "wait" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
fi
done
timeoutが発生するとどうなるのかみてみましょう。
#!/usr/bin/expect -f
set timeout 1
spawn ./wait_program.sh
expect "wait:"
send "wait\n"
interact
# result
# spawn ./wait_program.sh
# wait <--待たずして、送信しています。
# waitok
想定していなところで、コマンドが送信されてしまうこともありますので、
大きめの値にするか、無制限にすることをお勧めします。
後述で、expectコマンドの応用編で、対策も説明しますので、引き続きお楽しみください。
spawn
spawnコマンドは、Expect内でプロセスを起動します。
先ほどの課題であった、sshしてみようでは、sshのプロセスを動作させていました。
Expectでいう動作の始まりという認識で問題ないかと思います。
#!/usr/bin/expect -f
spawn ssh pi@192.168.11.48
expect "assword:"
send "raspberry\n"
interact
# result
expect
spawnで実行されたプロセスをレスポンスを読み取り、動作を考えてみましょう!
先ほど、SSHではパスワードを求められていたので、簡単に動作を判断することができました。
次のプログラムのようにある程度はレスポンスの内容はわかるけど、変化する、端末によっては異なる動作をするかもしれない場合を考慮してみましょう!
サンプルのプログラムになります。
#!/bin/bash
word=("wait" "run")
for i in {1..5} ; do
# set ramdom number in 0 to 1
j=$(($RANDOM % 2))
echo ${j}
if [ ${j} -eq 0 ]; then
if [ ${word[${j}] } = "wait" ]; then
sleep 1
read -p "wait:" input_data
if [ ${input_data} = "wait" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
elif [ ${j} -eq 1 ]; then
if [ ${word[ ${j} ] } = "run" ]; then
read -p "run:" input_data
if [ ${input_data} = "run" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
else; then
sleep 20
fi
done
さきほどのExpectでは、waitしか待つことができませんでした。
expectコマンドは、複数の処理が可能です!
#!/usr/bin/expect -f
set prompt "(%|#|\\$) $"
spawn ./wait_program.sh
expect {
"wait:" {
send "wait\n"
exp_continue
} "run:" {
send "wait\n"
exp_continue
}
}
ついでに、timeoutしてしまった際の動作も定義してしまいましょう!
#!/bin/bash
word=("wait" "run")
for i in {1..5} ; do
# set ramdom number in 0 to 1
# timeを起こさせるために % 3に変更
j=$(($RANDOM % 3))
echo ${j}
if [ ${j} -eq 0 ]; then
if [ ${word[${j}] } = "wait" ]; then
sleep 1
read -p "wait:" input_data
if [ ${input_data} = "wait" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
elif [ ${j} -eq 1 ]; then
if [ ${word[ ${j} ] } = "run" ]; then
read -p "run:" input_data
if [ ${input_data} = "run" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
else; then
sleep 20
fi
done
#!/usr/bin/expect -f
set prompt "(%|#|\\$) $"
spawn ./wait_program.sh
expect {
"wait:" {
sleep 1
send "wait\r"
exp_continue
}
"run:" {
sleep 1
send "run\r"
exp_continue
}
timeout {
puts "timeout"
exit
}
}
send
**send**はspawnで実行したプロセスに対して現プロセスに対してコマンドを送信する機能です。
sendに似たコマンドをいつくかありますので紹介したいと思います。
コマンド | エイリアス | 概要 |
---|---|---|
send | exp_send | 現プロセスにコマンドを送る -null はヌル文字を送る |
send_error | exp_send_error | 現プロセスでなく stderr に送る |
send_log | exp_send_log | ログファイルに送る |
send_tty | exp_send_tty | 現プロセスでなく /dev/tty へ出力を送る |
send_user | exp_send_user | 現プロセスでなく標準出力へ出力を送る |
この中でも利便性が高いコマンドは、send_logではないでしょうか。
log_file hogehoge
と書くことで、hoehogeのログファイルが作成され、そこにログが書き込まれる。
基本的に、標準出力を全てロギングしてしまう。ロギングを抑制するには、log_user 0
とすれば抑制され
log_user 1
とすれば、抑制が解除される。
#!/usr/bin/expect -f
set prompt "(%|#|\\$) $"
log_file "exp.log"
spawn date
# プロンプトには表示されないがログファイルには書き込まれる。
send_log "create log file"
expect -re $prompt
send_user "date"
exit
interact
interactは、プロセスの主導権をユーザに戻すコマンドです。
#!/bin/bash
word=("wait" "run" "interact")
for i in {1..5} ; do
# set ramdom number in 0 to 1
# timeを起こさせるために % 3に変更
j=$(($RANDOM % 3))
echo ${j}
if [ ${j} -eq 0 ]; then
if [ ${word[${j}] } = "wait" ]; then
sleep 1
read -p "wait:" input_data
if [ ${input_data} = "wait" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
elif [ ${j} -eq 1 ]; then
if [ ${word[ ${j} ] } = "run" ]; then
read -p "run:" input_data
if [ ${input_data} = "run" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
elif [ ${j} -eq 2 ]; then
if [ ${word[ ${j} ] } = "interact" ]; then
read -p "interact:" input_data
if [ ${input_data} = "interact" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
done
interactだけだと、そのあと主導権がユーザに移ってしまうので
interact -nobuffer -re "(.*)\r" return
を使うことで
一度入力した後は再度Expectに主導権が移ります。
#!/usr/bin/expect -f
set prompt "(%|#|\\$) $"
spawn ./wait_program.sh
expect {
"wait:" {
sleep 1
send "wait\r"
exp_continue
}
"run:" {
sleep 1
send "run\r"
exp_continue
}
"interact:" {
interact -nobuffer -re "(.*)\r" return
exp_continue
}
}
下記のプログラムとだと無限ループ的な動きをしてしまう...
気をつけていただきたい。
read で取得と同時に、expectが読み取ってしまい、
無限ループに...
送信した内容と同じものを待つのはよくないということですね...
#!/bin/bash
word=("wait" "run")
for i in {1..5} ; do
# set ramdom number in 0 to 1
j=$(($RANDOM % 2))
echo ${j}
if [ ${j} -eq 0 ]; then
if [ ${word[${j}] } = "wait" ]; then
sleep 1
read -p "wait" input_data
if [ ${input_data} = "wait" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
elif [ ${j} -eq 1 ]; then
if [ ${word[ ${j} ] } = "run" ]; then
read -p "run" input_data
if [ ${input_data} = "run" ]; then
echo "ok"
else
echo "wow"
fi
else
echo "No"
fi
fi
done
さきほどのExpectでは、waitしか待つことができませんでした。
expectコマンドは、複数の処理が可能です!
#!/usr/bin/expect -f
set prompt "(%|#|\\$) $"
spawn ./wait_program.sh
expect {
"wait" {
send "wait\n"
exp_continue
} "run" {
send "wait\n"
exp_continue
}eof{
exit
}-re $prompt
}
}
(おまけ)TCL言語について
expectはTCL言語の仕組みを取り入れて、コードを整形することが可能です。
expectはTCL言語で構築されていますので、TCLでできることはできます。
自動化するのに便利そうな機能を紹介
私が、学生時代に利用してた、ns3の一つ前のns2ではシナリオを書く際はtclで書く必要がありました。
余談でした(笑)
ファイル読み込み
tclでファイルを読み取ってみましょう。
下記のようなファイルを準備します。
name,age,gender
taro,25,man
yui,20,woman
bob,50,man
alice,18,woman
ファイルを読み込むには、
コマンド | 概要 |
---|---|
open | ファイルをバッファとして扱う [open $hogehoge r] |
read | ファイルの中身を読み取る |
#!/usr/bin/expect -f
set fp [open "file" r]
set file_data [read $fp]
send $file_data
close $fp
配列処理
カンマ区切りのファイルはカンマ区切りで扱いたいですよね。
ちゃんと扱えます。
#!/usr/bin/expect -f
set fp [open "file" r]
set file_data [read $fp]
set data [ split $file_data "," ]
puts $data
close $fp
# result
# name age {gender
# taro} 25 {man
# yui} 20 {woman
# bob} 50 {man
# alice} 18 {woman
# }
これは、想定外の動作ですね...
では修正しましょう
#!/usr/bin/expect -f
set fp [open "file" r]
set file_data [read $fp]
set data [ split $file_data "\n" ]
set db {}
foreach one $data {
set d [ split $one "," ]
lappend db $d
}
puts $db
close $fp
# result
# {name age gender} {taro 25 man} {yui 20 woman} {bob 50 man} {alice 18 woman} {}
理想的な形に変わりましたね!
配列の1つ目を見る方法を確認してみましょう。
#!/usr/bin/expect -f
set fp [open "file" r]
set file_data [read $fp]
set data [ split $file_data "\n" ]
set db {}
foreach one $data {
set d [ split $one "," ]
lappend db $d
}
puts [ lindex $db 1 ]
puts [ lindex $db 1 2 ]
close $fp
# result
# taro 25 man
# man
ファイル書き込み
ファイルに書き込んでみましょう
コマンド | 概要 |
---|---|
open | ファイルをバッファとして扱う [open $hogehoge w] |
puts ?channelId? string | ファイルに書き込む puts $fileId $data |
#!/usr/bin/expect -f
set data "mskmemory,50,man"
set fileId [open writefile w]
puts -nonewline $fileId $data
close $fileId