経緯
CLIツールを作っていたときに、コマンドの標準出力のうち、
パイプ先のコマンドに渡したい出力と渡したくない出力があるケースがでてきました。
以下のようなツールです。
何らかの処理を行い、プログレスバー風のアニメーションをプロンプトに表示し、
処理結果のファイル名を最後に出力するコマンドです。
コードは以下のような感じです。
Nimでの実装です。
import os, strutils, strformat
for i in 1..60:
# カーソルを1行上に移動し、行を削除するANSIエスケープシーケンス
echo "\x1b[1A\x1b[K\x1b[1A"
# 進捗の分数
let prog = &"[ {i:>2}/60 ]"
# 処理済みのバー
let leftBar = "\x1b[44m" & " ".repeat(i).join() & "\x1b[m"
# 空白のバー
let rightBar = " ".repeat(60 - i).join()
echo &"{prog} [ {leftBar}{rightBar} ]"
sleep 25
# 処理結果を格納したファイルパス
echo "/var/tmp/result.txt"
渡したくないのはアニメーションをしているプログレスバーの部分です。
最後のファイル名だけパイプ先に渡して、catなりvimなりで開きたかったんです。
これをそのままlessするとどうなるか?
以下のようになります。
こうなります。悲惨です。
プログレスバーの出力と、カーソル移動、カーソル行の削除のANSIエスケープシーケンスまでlessが補足しています。
これを回避する方法を調べて、解決したことを書きます。
類似ツールの調査
必要な出力と不要な出力を分けてパイプ先に渡しているツールとしてpecoが思い浮かびました。
なので、pecoのソースを調べることにしました。
pecoのソースを読んだ所、pecoのUI部分はtermboxが全部引き受けていることがわかりました。
termboxで軽くUIを作ってlessしてみたところ、termboxのUI出力はパイプ先に渡さないことがわかりました。
termboxのソースを読んだところ、以下の部分がその理由でした。
/dev/tty
を開いています。
tty
について全然把握していなかったので、ttyについて調べました。
以下のQiitaの記事がとてもわかりやすかったです。
実装
前述の実装を参考に以下のように修正しました。
--- mycmd.nim 2019-11-05 21:16:47.623075601 +0900
+++ mycmd2.nim 2019-11-05 21:20:30.929732374 +0900
@@ -1,5 +1,15 @@
import os, strutils, strformat
+var
+ tty = open("/dev/tty", fmReadWrite)
+ oldStdin = stdin
+ oldStdout = stdout
+ oldStderr = stderr
+
+stdin = tty
+stdout = tty
+stderr = tty
+
for i in 1..60:
echo "\x1b[1A\x1b[K\x1b[1A"
@@ -9,4 +19,9 @@
echo &"{prog} [ {leftBar}{rightBar} ]"
sleep 25
+tty.close()
+stdin = oldStdin
+stdout = oldStdout
+stderr = oldStderr
+
echo "/var/tmp/result.txt"
tty
で仮想端末を開き、stdin
,stdout
,stderr
を上書きします。
echoは上書きされたstdout
のほうに出力します。
一連の処理が終わったらtty
を閉じてしまい、上書き前のstdin
,stdout
,stderr
で元に戻します。
このように変更を加えたプログラムmycmd2
を実行してみます。
結果がわかりやすいようにnl
を使います。
最後の出力結果のみ、パイプ先のnlが処理するようにできました。
これで特定の出力だけパイプ先に渡せそうです。
実装例
まだ作りかけなのですが、今回得た知見を利用して
Nimでディレクトリツリーを表示するコマンドを作ってます。
選択したファイルを最後に出力するので、あとはパイプ先でよしなに使ってくれ、というツールにしようかと。
以上です。