こんな経験ない?
- 毎日同じサーバーにSSHでログインして、同じコマンドを打つのが面倒 ‐ CLIで標準入力かつ手動入力で操作するプログラム。入力ミスすると致命的なのでなんとかしたい。
- バックアップスクリプトを夜中に自動実行したいが、途中でパスワードがあり入力待ちで処理が止まる
昔こんな業務に携わっていることがありました。その時にはサーバとは別の操作端末なるものがあり、WindowsでTeraTermのマクロを機能を使って操作をしていました。それだけのためにWindowsPCがいるというのもイヤハヤといった時代でしたが、GUIがなくキーボード入力必須という環境、もしかしたら、まだありますかね?
古(いにしえ)の運用エンジニアでWindowsのTeraTermを使っている方なら、マクロ機能でこんな作業を自動化した経験があるかもしれません😫
そんなTeraTermマクロができることを、Linux(WSL含む)やmacOSのコマンドラインでもできるようにするのが、今回紹介するpexpect(Python)とexpect(Tcl)です。これらを用いることで、CLIで標準入力を行うプログラム(以降、対話的プロセスと呼びます)を全てプログラムで自動化できます。
これらのpexpectとexpectの両方について紹介します。
概要
pexpect、expectとは?
両者とも、目的は対話的プロセスの自動化を目的としています。
‐ pexpectは、Pythonで対話的プロセスを制御するライブラリです。特定の出力パターンを待機し、適切な入力を自動送信できます。 - expectは、Tclベースのコマンドラインツールで、pexpectの先祖となります。対話的自動化の標準的なツールとして長年使われています。
インストール方法
pexpectとexpectのインストール方法についてみていきます。expectはDocker経由で使用することもありそうなので、Alpine Linuxについてもインストールを書いておきました。Docker Composeなどで使用する可能性もあるかもしれませんね。
🐍pexpect(Python)のインストール方法
$ pip install pexpect
🪶expect(Tcl)のインストール方法
# Ubuntu/Debian $ sudo apt update $ sudo apt install expect # Alpine Linux(Docker用) $ apk add --no-cache expect
インストールの注意点
これらについては処理系が決まっています。基本的にはexpectのほうが用途役割は広いかなと思いますが、pexpectはPythonの豊富なライブラリ資源を使用することができるので、多彩なことができると思います。古いシステムではexpect、比較的新しいシステムではpexpect使うのがいいかもしれません。
※Windowsネイティブの対応はwinpexpectが存在。
基本的な使い方
🐍pexpectの基本的な使用方法
基本的には以下のように使用します。
import pexpect # 最初の例(コマンド実行) child = pexpect.spawn('ls -la') output = child.read() print(output.decode('utf-8')) child.close() # 対話的プロセスのパターンマッチングと応答 child = pexpect.spawn('python3') child.expect('>>>') child.sendline('print("Hello, pexpect!")') child.expect('>>>') output = child.before.decode('utf-8') print(f"出力: {output}") child.sendline('exit()') child.close()
上記の実行例は以下の様になります。
total 36
drwxr-xr-x 4 user2404 user2404 4096 Aug 7 23:25 .
drwxr-x--- 23 user2404 user2404 4096 Aug 7 23:29 ..
drwxr-xr-x 7 user2404 user2404 4096 Aug 7 23:25 .git
-rw-r--r-- 1 user2404 user2404 109 Aug 7 23:25 .gitignore
-rw-r--r-- 1 user2404 user2404 5 Aug 7 23:25 .python-version
drwxr-xr-x 4 user2404 user2404 4096 Aug 7 23:25 .venv
-rw-r--r-- 1 user2404 user2404 0 Aug 7 23:25 README.md
-rw-r--r-- 1 user2404 user2404 532 Aug 7 23:27 main.py
-rw-r--r-- 1 user2404 user2404 181 Aug 7 23:25 pyproject.toml
-rw-r--r-- 1 user2404 user2404 1703 Aug 7 23:25 uv.lock
出力: print("Hello, pexpect!")
Hello, pexpect!
🐍pexpect関数と属性の説明
関数
| 関数名 | 説明 |
|---|---|
pexpect.spawn(command) |
引数で設定したコマンドを子プロセスとして起動し、制御可能なプロセスオブジェクトを返す |
pexpect.spawn(command, timeout=秒) |
timeoutオプション付きでプロセスを起動。指定秒数でタイムアウトする |
child.read() |
プロセスオブジェクトからの出力を全て読み取ってバイト形式で返す |
child.close() |
子プロセスのオブジェクトを終了し、リソースを解放する |
child.expect(pattern) |
引数で指定したパターン(文字列や正規表現)が出力に現れるまで待機し、マッチした場合のインデックスを返す |
child.expect([pattern1, pattern2, ...]) |
複数のパターンをリストで指定し、最初にマッチしたもののインデックス(0から開始)を返す |
child.sendline(text) |
引数に指定したテキストに改行文字を付けて子プロセスのオブジェクトに送信する |
属性
| 属性名 | 説明 |
|---|---|
child.before |
expect()でマッチした部分より前の出力内容をバイト形式で格納する |
定数
| 定数名 | 説明 |
|---|---|
pexpect.TIMEOUT |
expect()でタイムアウトが発生した場合に返される値 |
例外
| 例外名 | 説明 |
|---|---|
pexpect.exceptions.EOF |
子プロセスが予期せず終了した場合に発生する例外 |
🪶expectの基本的な使用方法
以下のように使用します。
実行方法
$ expect ./expect_test.exp または $ chmod +x ./expect_test.exp $ ./expect_test.exp
プログラムの拡張子は.expとしますが、実際はTclのプログラムです。ただし、拡張子を.tclとしてもわかりにくいので.expとすることが多いと思います。また、プログラムの先頭のシバン(#!/usr/bin/expect -f)が重要なので間違えないようにしてください。
#!/usr/bin/expect -f # 基本的なコマンド実行 spawn ls -la expect eof # パターンマッチングと応答 spawn python3 expect ">>>" send "print('Hello, expect!')\r" expect ">>>" send "exit()\r" expect eof
🪶expect関数と属性の説明
関数
| 関数名 | 説明 |
|---|---|
spawn command |
指定したコマンドを子プロセスとして起動し、そのプロセスとの通信を開始する |
expect pattern |
指定したパターン(文字列や正規表現)が子プロセスの出力に現れるまで待機する |
expect { } |
複数の条件分岐を持つexpectブロック。複数のパターンに対して異なる処理を実行 |
send string |
指定した文字列を子プロセスに送信する |
set variable value |
変数に値を設定する |
puts string |
指定した文字列を標準出力に表示する |
exit code |
プログラムを終了する。終了コードを指定可能 |
特殊キーワード・記号
| キーワード・記号 | 説明 |
|---|---|
eof |
End of File の略。プロセスが終了して出力が完了するまで待機する |
timeout |
タイムアウト条件。指定時間内にパターンがマッチしなかった場合に実行される |
\r |
キャリッジリターン(改行文字)。Enterキーを押すのと同等の効果 |
$variable |
変数の値を参照する。setで設定した変数の内容を取得 |
{ } |
expectの複数条件分岐やコードのグループ化に使用 |
変数
| 変数名 | 説明 |
|---|---|
timeout |
expectの待機時間を秒単位で設定する特殊変数(デフォルト:10秒) |
こちらのほうがshファイルに近い表記かなと思います。
pexpectとexpectとの比較
記法・構文の比較
pexpectの場合のSSH自動ログイン実装の例
#!/usr/bin/env python3 import pexpect hostname = "example.com" username = "myuser" password = "mypassword" # SSH接続を開始 child = pexpect.spawn(f'ssh {username}@{hostname}', timeout=30) try: index = child.expect(['password:', 'yes/no', pexpect.TIMEOUT]) if index == 0: # password: child.sendline(password) child.expect('$ ') child.sendline('ls -la') child.expect('$ ') child.sendline('exit') elif index == 1: # yes/no child.sendline('yes') child.expect('password:') child.sendline(password) child.expect('$ ') child.sendline('ls -la') child.expect('$ ') child.sendline('exit') else: # timeout print("接続がタイムアウトしました") except pexpect.exceptions.EOF: pass child.close()
expectの場合のSSH自動ログイン実装の例
#!/usr/bin/expect -f set hostname "example.com" set username "myuser" set password "mypassword" set timeout 30 # SSH接続を開始 spawn ssh $username@$hostname # パターンマッチング expect { "password:" { send "$password\r" expect "$ " send "ls -la\r" expect "$ " send "exit\r" } "yes/no" { send "yes\r" expect "password:" send "$password\r" expect "$ " send "ls -la\r" expect "$ " send "exit\r" } timeout { puts "接続がタイムアウトしました" exit 1 } } expect eof
機能の比較
| 項目 | pexpect(Python) | expect(Tcl) |
|---|---|---|
| ベース言語 | Python | Tcl |
| 学習コスト | Python知識を活用 | Tcl習得が必要 |
| 機能性 | 豊富なPythonライブラリ | 限定的 |
| エラーの取り扱い | 高度(try-catch等) | 基本的 |
| 統合性 | アプリケーション組み込み | 単体ツール |
| 起動速度 | やや重い | 高速 |
| メモリ使用量 | 多い | 少ない |
使い分けの選択基準
現実的には、どちらも追加インストールが必要なので、用途によって選択するのが良いでしょう🤔 というと、元も子もないですけど。以下のような点を参考に選択するといいかなと思います。
pexpectを選ぶポイント
- 複雑なロジックや統合が必要
- エラーハンドリングが重要
- 長期的な保守性が求められる
- Pythonの豊富なライブラリを活用したい
- アプリケーションへの組み込み
expectを選ぶポイント
- シンプルで軽量な自動化が必要
- システム管理の単発タスク
- リソース制約がある環境
- 既存のシェルスクリプトとの統合
- 起動速度重視
よくある問題と解決策
1. 文字エンコーディングの問題
read()では読み込んだデータをバイト列として格納するので、画面出力時には明示的にエンコーディングを指定するのがよいでしょう。
child = pexpect.spawn('command') child.encoding = 'utf-8' # エンコーディングを明示的に設定 output = child.read() # 自動的にUnicode文字列に変換
2. プロンプトの認識問題
# より具体的なパターンで待ち child.expect(r'user@hostname:.*\$ ') # 正規表現を使用 # 複数パターンの組み合わせで待ち child.expect(['$ ', '#', '>>> ']) # 複数の可能性を考慮
3. タイムアウトの調整
# 長時間処理の待機 child.expect('pattern', timeout=300) # 5分 # タイムアウトを無効化 child.expect('pattern', timeout=None)
おわりに
両方とも対話的プロセスの自動化することが可能になります。また、自動化することで手作業を削減し、人的エラーを防ぐことができます。まずは小さな自動化から始めて、徐々に適用範囲を拡大していくのがいいかなと思います🙂