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

# Expectについて

ExpectはLinuxのCLIコマンドを自動化する化するパッケージの一つです。

普通のbashでも自動化できるじゃないですか。というご意見もあると思います。
実際間違ってはいません。対話型に特化しているツールとおもっていただきたいです。


[公式ドキュメント](https://linux.die.net/man/1/expect)
[日本語ドキュメント](https://linuxjm.osdn.jp/html/expect/man1/expect.1.html)　<--ここがわかりやすいです。



#### bashの簡単なおさらい

<details>
<summary>bashのおさらい</summary>
<div>

```bash:例(表示のみ)
#!/bin/bash

ls /var
# result
# backups  lib    lock  mail  run   spool  tmp
# cache    local  log   opt   snap  swap   www
```

変数の扱いについて

```bash:例（変数の扱い）
#!/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
```

```bash:例（for）
#!/bin/bash

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


```

<details open>
<summary> そのほかfor文 </summary>
<div>
色々なfor文の書き方について

```bash:例（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

```
</div>
</details>

```bash:例（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文この時点で色々なことができるでしょう。
次の例ではどうでしょうか。

</div>
</details>


## お題1
>sshでAサーバにログインしてください。

以下、ローカル「192..168.11.50」のラズパイサーバがあるとしましょう。

```bash:回答（1/2）
#!/bin bash

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

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

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


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

```bash:回答（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](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%90%E3%83%B3_(Unix))


bashではなく、expectとして同じものを書いてみましょう。

```tcl:expect例
#!/usr/bin/expect -f

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

```

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


# Expect のインストール

### CentOS7

```bash:install
$ yum install expect
```

### mac
```zsh:install
# brew install expect
```
[brewのインストールはこちら](https://brew.sh/)

# Expectの基礎構文

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

### set

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

```tcl:set.exp
#!/usr/bin/expect -f

set data "hello"
puts $data

# result
# hello
```





#### set timeout
**timeout**には、特集な意味を持つ変数です。
expectでbashからレスポンスを待つ時間を設定できます。

|項目| 値、設定方法|
|:-:|:-:|
|default|10 second|
|1秒に変更|set timeout １|
|３０秒に変更|set timeout 30|
|無制限|set timeout -1|

**初めに、timeoutが起こるプログラムを作成しましょう。**
~~みなさまの家庭には一家に３台はあるとい言われているrassberry piですが、我が家にはrassberry pi を１つしかありませんでした~~

```bash: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が発生するとどうなるのかみてみましょう。

```tcl: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191643/de6555ea-6fd1-755e-3219-197b86433ee9.jpeg)


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


```tcl: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191643/de6555ea-6fd1-755e-3219-197b86433ee9.jpeg)

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


```bash: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コマンドは、複数の処理が可能です！

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

```bash: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
```


```tcl: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|現プロセスにコマンドを送る<br> -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` とすれば、抑制が解除される。

```tcl: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**は、プロセスの主導権をユーザに戻すコマンドです。


```bash: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に主導権が移ります。

```tcl: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
  }
}


```



<details>
<div>

下記のプログラムとだと無限ループ的な動きをしてしまう...
気をつけていただきたい。

read で取得と同時に、expectが読み取ってしまい、
無限ループに...
送信した内容と同じものを待つのはよくないということですね...

```bash: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コマンドは、複数の処理が可能です！


```tcl: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
  }
}


```

</div>
</details>



# （おまけ）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 |ファイルをバッファとして扱う<br>[open $hogehoge r]|
|read |ファイルの中身を読み取る|

```tcl:read_file.exp
#!/usr/bin/expect -f

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

```

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

```tcl: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
# }

```

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

```tcl: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} {}

```

理想的な形に変わりましたね！
配列の１つ目を見る方法を確認してみましょう。

```tcl: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 |ファイルをバッファとして扱う<br>[open $hogehoge w]|
|puts ?channelId? string　|ファイルに書き込む <br> puts $fileId $data|

```tcl:write_file（初期化）
#!/usr/bin/expect -f
set data "mskmemory,50,man"

set fileId [open writefile w]
puts -nonewline $fileId $data
close $fileId
```