LoginSignup
12
11

More than 3 years have passed since last update.

expectを使って自動化してみよう。

Last updated at Posted at 2021-04-18

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
例(for)
#!/bin/bash

for i in {1..10} ; do
        echo ${i}
done
# result
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
# 10



そのほかfor文

色々なfor文の書き方について
例(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


例(if)
#!/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」のラズパイサーバがあるとしましょう。

回答(1/2)
#!/bin bash

ssh pi@192.168.11.50
# pi@192.168.11.48's password: <--入力を求められてしまう。

これでは自動化は難しいですね...
何百台とある場合の手作業は大変なものです。

あーなんとかして入力待ちを検知して、自動で入力してくれないものか...

ここで便利なのがexpectです。

回答(2/2)
#!/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として同じものを書いてみましょう。

expect例
#!/usr/bin/expect -f

set timeout 3
spawn ssh pi@192.168.11.50
expect "assword:"
send "raspberry\n"
interact

"" で囲わなくていいのでこちらのほうが楽かもしれませんね!
個人的には、expectで書く方法をおすすめしています。

Expect のインストール

CentOS7

install
$ yum install expect

mac

install
# brew install expect

brewのインストールはこちら

Expectの基礎構文

よく使うであろう、基礎構文について学んでみましょう!

set

setは、変数を宣言するときに利用します。
dataにhelloという文字列をいれてみましょう。

set.exp
#!/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つしかありませんでした

wait_program.sh
#!/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が発生するとどうなるのかみてみましょう。

expect(set_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内でプロセスを起動します。

qiita.001.jpeg

先ほどの課題であった、sshしてみようでは、sshのプロセスを動作させていました。
Expectでいう動作の始まりという認識で問題ないかと思います。

expect(spawn)
#!/usr/bin/expect -f

spawn ssh pi@192.168.11.48
expect "assword:"
send "raspberry\n"
interact

# result 

expect

spawnで実行されたプロセスをレスポンスを読み取り、動作を考えてみましょう!

先ほど、SSHではパスワードを求められていたので、簡単に動作を判断することができました。
次のプログラムのようにある程度はレスポンスの内容はわかるけど、変化する、端末によっては異なる動作をするかもしれない場合を考慮してみましょう!

qiita.001.jpeg

サンプルのプログラムになります。

wait_program.sh
#!/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コマンドは、複数の処理が可能です!

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してしまった際の動作も定義してしまいましょう!

wait_program.sh
#!/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
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
  }

  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 とすれば、抑制が解除される。

send_log_user.exp
#!/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は、プロセスの主導権をユーザに戻すコマンドです。

wait_program.sh
#!/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に主導権が移ります。

interact.exp
#!/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が読み取ってしまい、
無限ループに...
送信した内容と同じものを待つのはよくないということですね...

wait_program.sh
#!/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コマンドは、複数の処理が可能です!

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でファイルを読み取ってみましょう。

下記のようなファイルを準備します。

file
name,age,gender
taro,25,man
yui,20,woman
bob,50,man
alice,18,woman

ファイルを読み込むには、

コマンド 概要
open ファイルをバッファとして扱う
[open $hogehoge r]
read ファイルの中身を読み取る
read_file.exp
#!/usr/bin/expect -f

set fp [open "file" r]
set file_data [read $fp]
send $file_data
close $fp

配列処理

カンマ区切りのファイルはカンマ区切りで扱いたいですよね。
ちゃんと扱えます。

read_file_split.exp
#!/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
# }

これは、想定外の動作ですね...
では修正しましょう

read_file_split.exp
#!/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つ目を見る方法を確認してみましょう。

read_file_split.exp
#!/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
write_file(初期化)
#!/usr/bin/expect -f
set data "mskmemory,50,man"

set fileId [open writefile w]
puts -nonewline $fileId $data
close $fileId
12
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
11