ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Rubyで別のコマンドをラップしたい時、ptyライブラリが便利

library pty (Ruby 2.2.0)

pty とは、擬似端末(Pseudo tTY)を扱うライブラリ。

典型的には以下のように使う。

require 'pty'
status = nil
PTY.spawn("for i in $(seq 1 10); do echo progress $i; sleep 1; done") do |pty_out, pty_in, pid|
  pty_in.close
  while l = pty_out.gets
    puts "PTY: #{l}"
  end
  status = PTY.check pid
end
# PTY: progress 1
# PTY: progress 2
# PTY: progress 3
# ...
# PTY: progress 10
#=> nil

p status
#=> #<Process::Status: pid 91190 exit 0>

Kernal#spawn と違って、ブロックを伴うことができ、出力一行ごとに対して何かする、といった処理に向いている。

Open3.capture2 なども似たような機能を提供するが、こちらはttyがアサインされない。

Open3.capture2 'ruby -e "p STDOUT.tty?"'                                              
#=> ["false\n", #<Process::Status: pid 90573 exit 0>]

Open3系では擬似端末をアサインするオプションがないようだ(Open3.pipeline などでttyに繋いだりすればできるかもだがややこしいと思う)。一部のツールはttyにつながっている時とそうでない時とでログのフォーマットを勝手に変えたりする(Goのツールlogrus 使ってるやつとか)ので、困ってしまうことがある。そこでPTYが便利、という話でした。

追記、ttyの扱いの違いについて

Kernal#spawn
spawn "tty"
#=> 91755
# 少し遅れてttyが得られるが、これは今ログインしている端末のttyを受け継いでいることがわかる
# /dev/ttys010
Open3.capture2
Open3.capture2("tty")
=> ["not a tty\n", #<Process::Status: pid 91669 exit 1>]
PTY.spawn
PTY.spawn("tty") {|i, o, pid| o.close; while(l = i.gets); puts l; end; p PTY.check pid}
# /dev/ttys016
#=> #<Process::Status: pid 91708 exit 0>

PTY.spawn("tty") {|i, o, pid| o.close; while(l = i.gets); puts l; end; p PTY.check pid}
# /dev/ttys017
#=> #<Process::Status: pid 91709 exit 0>
# ...というように、毎回新しく作っている

詳細はソースで(ただし、C含む)