背景
リリースしたLinuxのルートファイルシステム等のビルドレシピをgit管理しているのですが、pushする際にSSHのパスフレーズを求められてしまうため、パスフレーズをちゃんと設定していると各レポジトリについてパスフレーズを打つ必要があります。打鍵回数で行を行う僧侶じゃないので、そんなことはしたくない!
expectを使う
久しぶりにexpectを持ち出してきたわけです。
expectでの自動化は、他の方も過去にまとめられていますね。
Linuxの対話がめんどくさい?そんな時こそ自動化だ!-expect編-とかも参考になさってください。
expectは、tcl拡張で主にターミナル内の出力を捕捉して、パターンマッチしたら任意の文字列を入力することができるものです。私も最後に使ったのってまだシリアルコンソールで設定をすることの多かった時代(10年以上前)で、文法を見事に忘れていて参ったので、今後のために?まとめたものです。
GUIの自動操作とかはPythonさんとかに任せておけばよいですが、端末への自動入出力をやらせるのはexpectが手っ取り早いので、しょうがないなぁと久々に動かしてみました。
基本動作要件
- 'Enter passphrase for key...:'という文字列が見えたら、SSHパスフレーズを入力して欲しい。
- コマンドプロンプトが見えたら、実行を終了したい。
- SSHパスフレーズはスクリプト内で決め打ちしたくはないな…(最初の1回だけは手入力させて欲しい)
まぁ、このくらいできればとりあえず何とか幸せになれそうです。
私の場合
expectのスクリプトはexpectの中で閉じるように書いています(引数管理とかをシェルに任せて、シェルスクリプト内でexpect -cを呼ぶやり方もあり得ますが、それをするとエスケープシーケンスを何個も書く必要に迫られてしんどいのです)
大枠
変数等の設定
以下二つくらいは設定しておくと、後で幸せです。
変数名 | 意味 |
---|---|
log_file | ログファイルへの書き出し。ファイル名指定。 |
timeout | タイムアウト時間(文字列マッチするまでに待って良い時間) |
私の場合、gitにタグを打ちたい、ですので、大きなレポジトリにタグを打つと、マニュアル等で載っている5秒とかだとタイムアウトにすぐなってしまって悲しいので、私は10分(600秒)とか豪快な数にしました。2分ですと、ちょっとまだ短かったので。
使いたいコマンドもしくはシェルスクリプトをspawnする
こんな感じですね。
expect: git-tag.exp
spawn /bin/bash -ex /path/to/your/shell/script
expectループの中で、文字列マッチを書いていく。
この文字列が出力されてきたらこうする、という動作をブレースで括って書きます。if文などの分岐も書けますが、その条件部分がブレース囲み({})なので、他の言語に詳しい人は間違えます。
文字列マッチの書き方
spawnを打った後に、以下のようなループを書きます。ループ内で、-re "..."となっているところが端末出力のパターンになっていて、マッチしたら、ブレースの中の処理が実行されます。
expect {
-re "^.*passphrase.*\ $" { # 各パターンごとの処理を書きます。
# パスフレーズを入れる処理
exp_continue # まだ処理は終わっていないので、continueする。
}
-re "^$Username@.*\$" {
# プロンプトに戻ったときの処理
}
timeout {
# タイムアウト時の処理
}
}
ここで、普通文字列に完全一致するものを書くことは稀かと思います。正規表現もしくはblob形式で書くのが普通でしょう。それを先頭で指定しています。
スイッチ名 | 意味 |
---|---|
-re | 正規表現をパターンに書いてます、という意味 |
-blob | blob表現をパターンに書いてます、という意味 |
私は、慣れの問題もあって、-reばかり指定していますね…。正規表現を覚えていると、まぁ大抵のスクリプト言語で通用しますので(数量指定({2,6}とか)はできないこともありますが、あんまり複雑なのを書くこともないので)。
パスフレーズはスクリプト内に書きたくないなーという処理
スクリプト内にバッチリパスフレーズを書いちゃうと、それを他人に見られたとき、何のためのパスフレーズなのよ、とセルフツッコミしたくなってしまいますので、今回は初回だけユーザがインタラクティブに入力して、その後は入力されたパスフレーズを使い回す、というようにしてみました。
set Username [lindex $argv 0]
set Passphrase [lindex $argv 1]
expect {
-re "^.*passphrase.*:\ $" {
if {$Passphrase == ""} {
stty -echo # 入力を表示させない
send_user "passphrase (for $Username): "
expect_user -re "(.*)\n" # ユーザ入力を改行の手前まで取得
set Passphrase "$expect_out(1, string)\r" # パスフレーズをセット
send $Passphrase # 実際に端末に入力する
stty echo # 入力エコー復活
} else {
send $Passphrase\r # 一度打っていたら、それを使い回す
}
exp_continue
}
}
第1引数の値をユーザ名としておいて、第2引数をパスフレーズとして取ります。第2引数を指定しないときは一度だけパスフレーズ入力を要求して(ただし、echo出力はさせない)、それを使い回す、という風にしています。
set Username [lindex $argv 0]のところはtcl特有で、ドはまりポイントの一つです。[]で囲うと、その中にあるコマンドで置換されます。つまりまぁ、argv配列の0番目の要素をUsernameという変数に指定する、という意味ですが、初見だと訳が分かりませんねー。
今回は、とりあえずこれだけ行えれば文句なかったので、マニュアル見て試行錯誤した割にはできあがったスクリプトはとても簡素なことになってしまいました。
#!/usr/bin/expect
log_file git-tag-expect.log
if {$argc > 2 || $argc < 1} {
puts {Usage: git-tag.exp <Username> [pass phrase]}
exit 1
}
set timeout 600
set Username [lindex $argv 0]
set Passphrase [lindex $argv 1]
spawn /bin/bash -ex git-tag.sh # 動かしたかったシェルスクリプトを指定
expect {
-re "^.*passphrase.*:\ $" {
if {$Passphrase == ""} {
stty -echo
send_user "passphrase (for $Username): "
expect_user -re "(.*)\n"
send_user "\n"
set Passphrase "$expect_out(1,string)\r"
send $Passphrase
stty echo
} else {
send $Passphrase\r
}
exp_continue
}
-re "^$Username@.*\$" {
break
}
timeout {
puts {"timeout occurred..."}
break
}
}
終わりに
まぁ、二度と自分がこの程度のことでハマらないように、という自戒を込めて簡単なexpectスクリプトの書き方を書いておきました。端末入出力の自動化をしたいとき、下準備がほとんど要らないので(debianとかならapt-get install expectでささっと入ります)使おうという気になる………かもしれません。