expect TIPS

# `expect` TIPS

unix系OSでコマンドライン対話を自動化するツール `expect` に関するTIPSです。
`expect` 特有の話と、Tcl の話が混ざってます。
なんとなく、ルーター、L2スイッチ、L3スイッチに対する対話を想定しています。

## `expect` は Tcl である

`expect` のスクリプトは Tcl(ティクル) 言語をベースにしています。
もっと言うと、`expect` は Tcl 言語の拡張機能です。

https://en.wikipedia.org/wiki/Expect

> Expect is an extension to the TCL scripting language written by Don Libes.

## 基本

### 変数への代入

```tcl

set MY_VAR "yes"
```

### コマンド展開: `[]`

コマンドを `[]` で囲うと実行時に展開されます。

https://www.tcl.tk/man/tcl8.5/tutorial/Tcl5.html

`if` での判定や、`set` での代入にコマンドの実行結果を使いたいときは `[]` で文を囲います。

```tcl
# 環境変数 MY_VAR が存在したら何かする
if { [ info exists ::env(MY_VAR) ] } {
    # do something
}
# 最初のコマンドライン引数を、MY_VARに代入する
set MY_VAR [lindex $argv 0]
```

### 文字列リテラル

`$VAR` または `${VAR}` で変数を参照し、展開できます。

文字列リテラルの中でも、`[]` はコマンド展開されます。展開されたくないときは `\[`、`\]` のようにバックスラッシュでエスケープします。

```tcl
set PROMPT "\[a-zA-Z0-9\._-\]+#"
```

## フロー制御

### 条件分岐

```tcl
if { 式1 } {
    # 何かする
} elseif { 式2 } {
    # 何かする
} else {
    # 何かする
}
```

### ループ

```tcl
for { 初期化文 } { 判定式 } { 繰り返し時に実行する文 } {
    # ループで何かする
}

for { set i 0 } { i <= 10 } { incr i } {
    # i=0からi=10まで繰り返される
}
```

### ログファイル

## 通信ログをファイルに記録する

```tcl
log_file ファイル名
```

デフォルトだと追記モードです。追記したくない場合は `-noappend` を使います。

```tcl
log_file -noappend ファイル名
```

## ログファイルへの記録を止める

引数無しでコマンド実行すると、ログファイルへの記録が終了します。

```tcl
log_file
```

## 画面出力

### ログと標準出力に出力する: `send_user`

`send_user` で標準出力に文字列を出力できます。
`log_file` でファイル出力している場合、ログファイルにも記録されます。
改行は自動付与されないので、明示的に書く必要があります。

```tcl
# 標準出力とログファイルの両方に出力される
send_user "hello\n"
```

### 標準出力に出力する: `puts`

`puts` でも標準出力に文字列を出力できます。
`log_file` でファイル出力している場合、ログファイルには記録されません。
改行が自動付与されます。

```tcl
# 標準出力だけに出力される
puts "hello"
```

## 実践編

### telnet、sshで改行を送信するときは `\r` で送る

```
send "somedata\r"
```

`\n` ではなく `\r` です。

### コマンドライン引数を取得する

コマンドライン引数は `$argv` 配列に入っています。
配列は `lindex` 関数でアクセスします。
https://www.tcl.tk/man/tcl8.5/TclCmd/lindex.htm

```shell
$ expect -f myscript.expect arg1 arg2 arg3
```

```tcl
if { [ lindex $argv 0 ] == "arg1" } {
    send_user "first arg is \"arg1\""
}
```


### `spawn` のコマンドラインを表示させない

`-noecho` を使います。

```tcl
spawn ssh 10.0.0.1
# expect の実行時に "spawn ssh 10.0.0.1" と表示される
```

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

```tcl
if { [ info exists ::env(MY_VAR) ] } {
    # do something
}
```

### デバッグする

`exp_internal` を使うと `expect` の内部状態の遷移が標準出力に出力されます。

```tcl
exp_internal 1
```

### `expect`コマンドで複数のパターンを待つ

```tcl
expect {
    "login:" {
        send "$USERNAME\r"
    }
    "password:" {
        send "$PASSWORD\r"
    }
}
```

"login:" が来たら USERNAME変数の値を送信します。
"password:" が来たら PASSWORD変数の値を送信します。

どっちもこなかったら、タイムアウトまで待ってエラーになります。

### `expect`コマンドで正規表現で待つ

パターンの前に `-re` を指定します。
`[]` はコマンド展開にならないようにエスケープが必要です。

```tcl
expect {
    -re "(username|login):" {
        send "$USERNAME\r"
    }
    -re "\[Pp\]assword:" {
        send "$PASSWORD\r"
    }
}
```

### 正規表現は tcl の方言

http://www.tcl.tk/man/tcl8.5/TclCmd/re_syntax.htm

### 正規表現の `.` は改行にはマッチしない

"partial newline-sensitive matching" になっているっぽい。

partial newline-sensitive matching

- `.` は改行文字 (`\r`, `\n`) にはマッチしない
- `^` は入力文字列の先頭にのみマッチする。途中の各行の先頭にはマッチしない。
- `$` は入力文字列の末尾にのみマッチする。途中の各行の末尾にはマッチしない。

### 正規表現のエスケープは二重エスケープにする

tcl文字列として評価された後に、正規表現としての評価になるので、二重にエスケープが必要です。

```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条件) で並べて `(?:)` (非キャプチャグルーピング) で囲う。

```tcl
expect {
    -re "(?:^|\[\\r\\n\])Password:" {
        # 何かする
    }
}
```

または、`(?n)` で newline-sensitive matching を指定して、`^` にマッチさせる。
接続先のOS依存だが、`\r\n`で改行した後に、`\r` で行頭にカーソル移動させて表示させていることがあるので、`\r?` も入れておくと無難です。

```tcl
expect {
    # 改行後に `\r` が入っている場合でもマッチさせる
    -re "(?n)^\\r?Password:" {
        # 何かする
    }
}
```


### `expect`コマンドでマッチした部分を取り出す

`$expect_out(0,string)` でマッチした全体を取り出します。
`$expect_out(1,string)` で 一つ目の `()` の中身を取り出します。

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