対話型プロンプトという壁
標準的なシェルスクリプトは逐次処理には最適ですが、コマンドがリアルタイムの対話を要求する場合、しばしば行き詰まってしまいます。
スクリプトが途中で停止し、パスワードの入力、ライセンス条項への同意、あるいは「yes/no」の選択を待っている状態のことです。sshやftp、あるいは独自のインストーラーなどのコマンドは、セキュリティ上の理由からTTY(テレタイプ)セッションを明示的に探すため、標準入力のリダイレクト(例:echo "password" | command)を無視することで知られています。
ここで、Expectが救世主となります。1990年代初頭に開発されたExpectは、Tcl言語をベースにした、対話型プログラムとやり取りするために設計されたツールです。特定の文字列(「プロンプト」)を監視し、事前に定義された応答を送信します。これは、ターミナルの前に仮想ロボットが座って代わりにタイピングしてくれているようなものです。APIが利用できず、SSHキーも使用できないレガシーシステムにおいて、私はこれを不可欠なものだと感じています。
システムへのExpectの導入
自動化を始める前に、ツールが必要です。Expectは最小構成のLinuxディストリビューションには必ずしもプリインストールされていませんが、ほぼすべての主要なリポジトリで利用可能です。インストールは簡単で、1分もかかりません。
Debian、Ubuntu、Linux Mintの場合:
sudo apt update
sudo apt install expect -y
RHEL、Fedora、AlmaLinuxの場合:
sudo dnf install expect -y
インストールを確認するには、単にバージョンをチェックします。私の現在の環境では、これから行うすべての作業において非常に安定しているバージョン 5.45.4 が動作しています。
expect -v
初めてのExpectスクリプトの作成
Expectスクリプトは通常、Expectインタプリタを指すシバン(shebang)から始まります。Tclの構文を借用していますが、効果的な自動化を書くためにTclのエキスパートである必要はありません。ほとんどのスクリプトは、spawn、expect、send、interactという4つの主要なコマンドを中心に構成されます。
コアコマンド:Spawn、Expect、Send
SSHログインの自動化という典型的な例を見てみましょう。これは、公開鍵認証をサポートしていない古いネットワーク機器を扱う際の、よくある課題です。
#!/usr/bin/expect -f
# 変数を設定
set timeout 10
set host "192.168.1.50"
set user "admin"
set password "Secret123!"
# プロセスを開始
spawn ssh $user@$host
# パスワードプロンプトを待機
expect "password:"
# パスワードを送信し、その後にキャリッジリターン(Enter)を送信
send "$password\r"
# 制御をユーザーに渡す
interact
このスニペットでは、spawnがSSHコマンドを開始します。expectコマンドは、ターミナルに “password:” という文字列が表示されるのを待ちます。見つかると、sendがパスワード文字列と、Enterキーをシミュレートする \r を送信します。最後に、interactがセッションを開いたままにするため、手動で操作を引き継ぐことができます。これがないと、スクリプトが終了した瞬間にSSHセッションも閉じてしまいます。
複数のプロンプトとロジックの処理
現実のシナリオはそれほど単純ではありません。接続が初めてかどうかや、エラーが発生したかどうかによって、異なるプロンプトに遭遇することがよくあります。私は、Expectブロック内で switch のような構造を使用して、これらのバリエーションを処理するのが好みです。
spawn ssh $user@$host
expect {
"yes/no" {
send "yes\r"
exp_continue
}
"password:" {
send "$password\r"
}
timeout {
puts "接続がタイムアウトしました!"
exit 1
}
}
exp_continue コマンドは便利なトリックです。これはExpectに対し、応答を送信した後もさらにプロンプトを待ち続けるよう指示します。これにより、SSHフィンガープリントの確認(”yes/no”)を処理し、その直後に続くパスワードプロンプトも処理できるようになります。
シェルからの引数の受け渡し
パスワードやホスト名をハードコードするのは悪い習慣です。argv 配列を使用することで、Bashシェルから引数をExpectスクリプトに渡すことができます。これにより、異なる環境でスクリプトを再利用できるようになります。
#!/usr/bin/expect -f
set user [lindex $argv 0]
set host [lindex $argv 1]
set password [lindex $argv 2]
spawn ssh $user@$host
expect "password:"
send "$password\r"
interact
このスクリプトは、./myscript.exp admin 10.0.0.1 mypass のように呼び出します。ただし、引数として渡されたパスワードはシェルの履歴に残る可能性があるため, このアプローチには注意が必要です。
スクリプトの検証と信頼性の確保
自動化は信頼性があってこそ価値があります。ソフトウェアのアップデートやターミナルの色設定の違いによってプロンプトがわずかに変わり、スクリプトが「脆く」なって失敗するのを何度も見てきました。
exp_internal によるデバッグ
Expectスクリプトがハングする場合、通常は表示されない文字列を待っていることが原因です。Expectが実際に何を見ているのかを正確に把握するには、スクリプトの冒頭に exp_internal 1 を追加してください。これにより、内部のマッチングプロセスがターミナルにダンプされ、どこで不一致が起きているのかが正確に示されます。
現実的なタイムアウトの設定
デフォルトでは、Expectのタイムアウトは10秒です。データベースの移行やリモートインストールのような時間のかかるタスクには、これでは短すぎることがよくあります。set timeout 300(5分)でグローバルタイムアウトを設定するか、-1 を使用して無期限に待機させることができます。ただし、無期限に待機するのは危険です。プログラムがクラッシュした場合、スクリプトが永久にハングしてしまうからです。
ローカルテストの重要性
3年間で10以上のLinux VPSインスタンスを管理してきた経験から、本番環境に適用する前に必ず徹底的にテストすることを学びました。これはExpectスクリプトにおいて特に当てはまります。期待する文字列のわずかなタイポが原因で、スクリプトが間違ったパスワードを何度も入力し、サーバーのセキュリティロックアウトを誘発する可能性があります。まずはローカルの仮想マシンやステージング用のコンテナでテストすることを強くお勧めします。
代替案:Autoexpectの使用
特に複雑な対話型ツールを扱っている場合、手動でスクリプトを書くのは退屈な作業です。マクロレコーダーのように動作する autoexpect というツールがあります。autoexpect ./your_command を実行して対話的な手順を手動で行うと、script.exp ファイルが生成されます。生成されたファイルは通常、少し煩雑でコメント過多ですが、複雑なワークフローの出発点としては最適です。
Expectをツールキットに統合することで、Linux管理の方法が一変します。手動介入と完全自動化のギャップを埋め、レガシーな対話型プロンプトに妨げられることなくワークフローを拡張できるようになります。ただし、セキュリティを常に念頭に置いてください。スクリプトの権限を保護し(chmod 700)、SSHキーやVaultトークンのようなより安全な代替手段がある場合は、プレーンテキストのパスワードを避けるようにしましょう。

