
こんにちは、tomorrowkeyです。
普段はAndroidアプリを書いていますが、100% Androidアプリ開発というわけでもないので、今回の記事ではAndroidではない話題を書いてみようかなと思います。
開発の現場ではたくさんのワークフローを実行しないといけない場面があり、それらをいつまでも放っておくとやらないといけないことが積み上がってしまい、本質的な開発に集中できません。
そういった単純な繰り返し実行を行うようであれば自動化をしたくなるのがソフトウェアエンジニアではないかと思います。単純にコマンドラインであればシェルスクリプトを書くことで自動化のワークフローを組むことができます。
もし自動化したいコマンドラインが実行中にパスワードの入力などインタラクティブな入力を求めるようなものではどうでしょうか。そのときにどのような手段をとれるでしょうか。
expect が使えるならそれが楽
Unix/Linux系の環境では、expect というツールが有名で、多くのユースケースで活用できます。
しかし、今回私が遭遇したシチュエーションでは、Windows環境で実行が必要なコマンドラインなうえに、そのコマンドがWSLに対応していないという状況だったため、 expect を使うことはできませんでした。
そこでRubyを使って expect 相当の実装をすることで課題に取り組みました。
基本的な考え方
インタラクティブなコマンドラインといっても、分解してみれば標準出力と標準入力のやり取りです。つまり
- プログラムが標準出力にメッセージを出力
- ユーザーが標準入力で応答
- この繰り返し
この仕組みが理解できれば、外部プロセスの標準入出力を扱えるプログラミング言語なら何でも自動化が可能です。
RubyのOpen3.popen3を使った実装
RubyのOpen3ライブラリを使うと、外部プロセスの標準入力、標準出力、標準エラー出力を個別に制御できます。
基本的な使い方
1行だけの標準出力があったうえで、標準入力が求められるコマンドラインを扱うなら、このような実装で自動化ができます。
require 'open3' Open3.popen3('interactive_command') do |stdin, stdout, stderr, wait_thr| # 標準出力から出力を読み取り output = stdout.gets puts "プログラムからの出力: #{output}" # 標準入力に応答を書き込み stdin.puts "yes" # プロセスの終了を待つ exit_status = wait_thr.value end
すごく単純ですが expect の原始的な実装ができました。
より実践的な実装例
expect コマンドのエッセンスを取り入れ、より実践的な実装に近づけるならば、標準入力の直前の標準出力を待つように実装するとよいです。
require 'open3' def automate_password_prompt(command, password) Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| # パスワードプロンプトが表示されるまで待機 while line = stdout.gets puts line if line.include?('Password:') # パスワードを送信 stdin.puts password break end end # 残りの出力を表示 while line = stdout.gets puts line end wait_thr.value end end
より複雑な対話の自動化
例えば複数の入力が必要なケースではどうでしょうか。
標準入力する直前のキーワードと、そのときに入力したい内容をペアで設定できるようにするととても便利です。
ここまでくると expect と遜色ないくらいの機能を持つ実装になってきました。
require 'open3' class InteractiveAutomator def initialize(command) @command = command @responses = {} end def add_response(prompt_pattern, response) @responses[prompt_pattern] = response end def execute Open3.popen3(@command) do |stdin, stdout, stderr, wait_thr| while wait_thr.alive? # 標準出力から1行読み込む line = stdout.gets next unless line puts line # 定義されたパターンにマッチするか確認 @responses.each do |pattern, response| if line.match(pattern) # 対応する入力を送信する stdin.puts response break end end end wait_thr.value end end end # 使用例 automator = InteractiveAutomator.new('deployment-tool') automator.add_response(/Enter environment/, 'production') automator.add_response(/Confirm deployment/, 'yes') automator.add_response(/Enter API key/, ENV['API_KEY']) automator.execute
実践するうえでの注意点
エラー出力
メインスレッドで標準出力、標準入力を扱っていると標準エラー出力が無視されてしまいます。 標準エラー出力は実行ログに出力させるだけであれば別スレッドで継続してパススルー出力させておくとよいです。
Thread.new do while !stderr.eof? line = stderr.gets $stderr.puts line end end
秘匿値の管理
例でも何度か触れているとおり、こういったインタラクティブな入力でよくあるのが、なにかしらのパスワードの入力です。 コード上にハードコーディングしてしまうと、それをコミットしてGitHubにアップロードしてしまう事故に繋がる危険性があります。 せめて環境変数に逃がして、コードからはそれを使うようにすると安心して管理できます。
まとめ
expectが使えない環境でも、RubyのOpen3を使えばインタラクティブなコマンドラインの自動化が可能です。 Rubyの実行環境があればどのプラットフォームでも動くことや、複雑なパターンにも対応が容易であることが非常に魅力的なソリューションです。 私が実際に使用したのは手元にあるWindows上での実行ですが、例えばCIで自動化させることを考えると、人間の手を離れたところで自動化させることができるのでワクワクしてきますよね。
もしインタラクティブなコマンドラインの自動化で困ったときは、ぜひこのアプローチも検討してみてください。