Capistranoと仮想端末と標準入出力の関係

次の二つのレシピの違いを考えてみる。

# (A)
task :foo do
  run "echo hello, world | dd bs=1 count=5 skip=5"
end
# (B)
set :default_run_options, :pty => true
task :foo do
  run "echo hello, world | dd bs=1 count=5 skip=5"
end

字面の上での違いは(B)の一行目、setで始まる行が(A)にはない点である。送信されるコマンドラインは同じであるから動作は同じかというと、そうでもない。これは実行してみるとわかる。だが、その前にrunで実行しているコマンドddの動きについて確認しておこう。ddはif=やof=で入力元・出力先を指定しなければ標準入力・標準出力に対して読み書きを行う。そして、読み書きを行った状況を標準エラー出力に書き出す。

上の(A)および(B)においては、echoにより標準入力に流し込まれた文字列を読み取り、オプションで指定された部分(6バイト目からの5バイト)を標準出力に書き出す。それでは実行結果を見てみよう(ある実行例からの抜粋)。

# (A)
  * executing "echo hello, world | dd bs=1 count=5 skip=5"
    servers: ["localhost"]
    [localhost] executing command
 ** [out :: localhost] , wor
*** [err :: localhost] 5+0 records in
*** [err :: localhost] 5+0 records out
*** [err :: localhost] 5 bytes (5 B) copied, 2.9705e-05 s, 168 kB/s
    command finished
# (b)
  * executing "echo hello, world | dd bs=1 count=5 skip=5"
    servers: ["localhost"]
    [localhost] executing command
 ** [out :: localhost] , wor5+0 records in
 ** [out :: localhost] 5+0 records out
 ** [out :: localhost] 5 bytes (5 B) copied, 3.03e-05 s, 165 kB/s
    command finished

似たような出力になっているが、よく見ると(A)には「out」と「err」があり、(B)には「out」しかない(一行少ないというのもあるが、それはまた別の話)。この違いはコマンド実行の際に仮想端末を要求したかどうかによる。(B)は仮想端末を要求していて、そのため、ddが標準出力に書き出した内容と標準エラー出力に書き出した内容の両方が、Capistranoには標準出力に書き出されたものに見えている。

とはいえ、これはCapistranoに限った話ではない。たとえば以下のコマンド実行例でも同じ状況を見てとることができる。

$ ssh -q    localhost "echo hello, world | dd bs=1 count=5 skip=5" >/dev/null
5+0 records in
5+0 records out
5 bytes (5 B) copied, 3.4422e-05 s, 145 kB/s
$ ssh -q -t localhost "echo hello, world | dd bs=1 count=5 skip=5" >/dev/null
(何も表示されない)

話を戻すと、こうした動作の違いは以下のときに問題になる可能性がある。

  • runやsudoにブロックを与えてコマンドの出力(特に標準出力)を取り込もうとしている
  • エラーが発生した(標準出力に何か書き出される)

どんなときにでも使える回避策というのはなさそうに思える。強いていえば次のどれか、またはすべてに気を付ける。

  • 標準エラー出力が不要であればコマンドライン上でリダイレクトして捨てておく
  • 受け取ったデータのうち必要と思われる部分だけをうまく抜き出す
  • 本当に必要なときにだけ:pty => trueを指定する

最後の点については、要するに:defaultrunoptionsに:pty => trueを入れるのは要注意だということになる。実際には、あらかじめ用意されたレシピを使うためなど、どうしようもないこともあるのだが、コントロールできるなら個々のrunやsudoにおいて:pty => trueを指定するといくらかマシになる。