コマンド対話ユーティリティのexpectの使い方の解説はよく見るのですが,そのライブラリであるlibexpectに関するサンプルは,man libexpect以外ではほとんど見かけません.C言語で書かれたプログラムからexpectを使ってtelnetするためのライブラリ関数の使い方を記録しておきます.
ライブラリのインストール
Centos7では $sudo yum install expect-devel
Ubuntu16では $sudo apt install tcl-expect-dev
ヘッダファイルとリンカーオプションの書き方
Centos7では #include でリンカーオプションに-lexpect
Ubuntu16では #include でリンカオプションに -expect -ltcl8.6
telnetでログインしてスーパユーザに成り上がって,ログをgrepするサンプル
#include <expect.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define UNIX_PROMPT "$ "
#define SU_PROMPT "# "
static int stage;
int test_telnet(char *host){
int fd;
const int OK=99;
char buff[128];
exp_loguser = 1;
exp_timeout = 3;
stage=10;
fd=exp_spawnl("telnet","telnet",host,"23",NULL);
if(fd<0){
return -10;
}
stage=20;
if(OK!=exp_expectl(fd,exp_glob,"login:",OK,exp_end)){
goto err;
}
strcpy(buff,"user1\n");
write(fd,buff,strlen(buff));
stage=30;
if(OK!=exp_expectl(fd,exp_glob,"Password:",OK,exp_end)){
goto err;
}
strcpy(buff,"pasword1\n");
write(fd,buff,strlen(buff));
stage=40;
if(OK!=exp_expectl(fd,exp_glob,UNIX_PROMPT,OK,exp_end)){
goto err;
}
strcpy(buff,"export LANG=c\n");
write(fd,buff,strlen(buff));
stage=50;
if(OK!=exp_expectl(fd,exp_glob,UNIX_PROMPT,OK,exp_end)){
goto err;
}
strcpy(buff,"su\n");
write(fd,buff,strlen(buff));
stage=60;
if(OK!=exp_expectl(fd,exp_glob,"Password:",OK,exp_end)){
goto err;
}
strcpy(buff,"password2\n");
write(fd,buff,strlen(buff));
stage=70;
if(OK!=exp_expectl(fd,exp_glob,SU_PROMPT,OK,exp_end)){
goto err;
}
strcpy(buff,"grep login /var/log/messages\n");
write(fd,buff,strlen(buff));
stage=70;
if(OK!=exp_expectl(fd,exp_glob,SU_PROMPT,OK,exp_end)){
goto err;
}
strcpy(buff,"exit\n");
write(fd,buff,strlen(buff));
stage=80;
if(OK!=exp_expectl(fd,exp_glob,UNIX_PROMPT,OK,exp_end)){
goto err;
}
strcpy(buff,"exit\n");
write(fd,buff,strlen(buff));
stage=99;
err:
close(fd);
return stage;
}
int main(){
test_telnet("localhost");
printf("\nresult=%d\n",stage);
return 0;
}
ソースコードのポイント
1.状態進行度を表す変数
スクリプトではなくC言語のようなプログラムからexpectを使うのは,別スレッドなどからexpctを使って対話する子スレッドの進行状況をモニターして,その状況を別のシステムに渡したい状況なので,stage変数で,どの段階まで進んだのかを示しています.
2.対話する相手を起動する
expectは,通常なら標準入出力で操作者と対話するプロセスを相手に,操作者の代理として対話してくれます.exp_spwanlは対話する相手となるプログラムを起動するために使われるライブラリ関数です.第1と第2パラメータには,起動プログラムをセットします.第1パラメータがファイル名,第2パラメータがコマンド名ですが,パスが通っているのでしたら通常は同じ内容になります.パスが通っていなければ第1パラメータだけフルパス,もしくは相対パス付きファイル名を指定します.第3パラメータ以降に,そのプログラムの起動パラメータをセットします.最後にNULLでパラメータ記述が終わったことを明示します.exp_spawnlでファイルディスクリプタが返されます.openシステムコールと同様,負数はエラーです.
3.対話する相手に話かける方法
このファイルディスクリプタに直接writeすると,対話相手のプログラムに入力できます.
4.対話する相手からの話を聞く方法
コマンド応答の読み込みは直接readするのではなく,exp_expectlライブラリ関数で読みます.対話型プログラムの場合,場面に応じて次にどういう内容が来るはずだろう,という期待する文字列があります.期待する文字列は,一つではなくいくつかの種類があるかもしれません.何かコマンドを投入して応答を待つ場合,成功した場合には成功のメッセージが,エラーの場合にはエラーメッセージが返されるものとします.場合によっては,成功時には応答メッセージはなく,プロンプトが表示されるだけの場合もあります.そこで,exp_expectlはプロンプトを含む一つ以上の応答メッセージ候補を待ち受けることができます.第一パラメータは読み取るファイルディスクリプタで,第2パラメータ以降は3つのパラメータを1セットとして一つの応答メッセージに対応して待ち受けを指定できます.
一つ目のパラメータは,メッセージの一致パターンの指定で,LINUXでの応答メッセージはexp_globで部分一致を指定できます.数値を含むパターンなどを正規表現で一致検査したい場合にはexp_regexpがしていきます.二つ目のパラメータは,応答メッセージに含まれる部分文字列で,3つ目のパラメータはその文字列が応答メッセージに含まれるときに,exp_expectlが返す値となっています.このようにexp_expectlは不定長パラメータを受け取りますので,最後のパラメータはexp_endとして,これ以上はパラメータがないことを指示します.
下のコードは,NetworkManagerのnmcliコマンドでPPPoEのアカウントを設定して応答メッセージの待ち受けをしている例です.nmcliコマンドは,成功すると,Connectionで始まるメッセージが返り,特にaddコマンドの場合には,successfully addedが応答メッセージに含まれます.
Connection 'PPPoE' (7b78fa59-26b2-44fe-a719-980bbbf3e72a) successfully added.
コマンド実行に失敗すると以下のような,Errorで始まるメッセージを受信します.
Error: invalid . 'usernane'.
これをexp_expectlに読み取らせ,エラーの場合は100を,成功時には200を返すように指示したコードが以下のようになります.
strcpy(buff,"nmcli c add type pppoe ifname eth1 con-name PPPoE username...");
write(fd,buff,strlen(buff));
ret=exp_expectl(fd,
exp_glob,"Error",100,//<=エラー発生時に期待されるメッセージ
exp_glob,"successfully added",200,//<=成功時に期待されるメッセージ
exp_glob,"#",99,//<=プロンプト
exp_end);
if(100==ret){
printf("Error\n");//<=エラーメッセージ発生時ここにきます
}else if(200==ret){
printf("OK\n");//<=成功時ここにきます
}else if(99==ret){
printf("Prompt\n");//<=応答メッセージがなくプロンプトが返ってくるとここにきます
}else{
printf("Unknown=%d\n",ret);//<=タイムアウトなどの発生時ここにきます
}
注意する点1 ロケール
ロケールが日本語になっていると応答メッセージが日本語になり,以下のような応答メッセージが読み込まれます.これではexp_expectlが正しく文字列一致検査できません.
接続 'PPPoE' (37c54453-26ab-47f8-bc53-64530fd73d15) が正常に追加されました。
エラー: 無効な . 'usernane' です。
そこで,ファイルディスクリプタを得られた直後に,write(fd,"LANG=c¥n",7);のようにロケールをデフォルトに変更しておく必要があります.
注意する点2 改行
telnetやshなどの対話型プログラムをexp_spawnlで起動した場合,対話の相手先はシェルになりますので,write(fd,"LANG=c¥n",7);のように,ファイルディスクリプタに書き込む文字列には改行文字を忘れないようにします.改行を忘れると,シェルはコマンド待ち状態を継続しますので,exp_expectlしてもタイムアウトします.
注意する点3 実行権限によるプロンプトの変化
一般ユーザ権限ではコマンドプロンプトは"\$"ですが,スーパーユーザでは"#"になります.libexpectで作ったプログラムが,スーパユーザ権限で起動されるのか一般ユーザで起動され,プログラムの内部でsuするのかで,最初の待ち受けプロンプトが異なりますので注意しましょう.exp_spawnlは複数のメッセージを待ち受けられますので,$と#のどちらでもOKなように待ち受けるのか,一方だけに限定するのかは,プログラムの仕様によります.