コマンドの出力を HTML に変換して貼り付ける

みなさんも生きていればコマンドの出力をブログに貼り付けたいということがあるでしょう. というわけでコマンド出力をなんとか HTML にして貼り付ける方法のご紹介です.

TL; DR: script -q /dev/null <command> | ansi2html -i | pbcopy


まず HTML は基本的には以下のような形で良いはずです. 色とかフォントとかは適当に変えたければ変える.

<pre style="color:#CCC;background-color:#000">
<!-- ここにコマンドの出力を貼り付ける -->
</pre>

例えば ls の結果を貼り付けたいとすると, 適当に pbcopy に pipe で流し込んでコピーすれば良さそうに思われます. この pbcopymacOS に標準で存在するコマンドで, 標準入力から受け取った内容をクリップボードにコピーします. 他の環境の場合は適当に置き換えて読んでください.

ls | pbcopy

これを貼り付けて完成. めでたし.

LICENSE
README.md
bin
coverage
jest.config.js
lib
node_modules
package.json
yarn-error.log
yarn.lock

で, 終わりではなくて, (ひょっとしたら環境によるかもしれないけれど) 普通に ls を実行したとき, こんな風に 1 行に 1 個のファイル / ディレクトリの形で出力されたでしょうか? おそらく以下のように, 1 行に複数個のファイル / ディレクトリが含まれる形になるのではないかと思います.

LICENSE     README.md   bin     coverage    jest.config.js  lib     node_modules    package.json    yarn-error.log  yarn.lock

なぜ出力が異なるかと言うと, コマンドは自身の出力が TTY 端末に対して行われているかを判別することができて, ls などコマンドによってはこれを見て出力を切り替えているのです.

例えば Node.js では以下のようなコードで判別できます. 出力が TTY 端末でない場合 (例えば pipe) に対して行われている場合は falsy な値 (undefined) が得られます.

$ node -e 'console.log(process.stdout.isTTY)'
true
$ node -e 'console.log(process.stdout.isTTY)' | cat
undefined

では TTY 端末に出力される結果をそのまま取得したい場合はどうしたら良いかというと, script コマンドを使ったトリックを使います. 例えば実行したいコマンドが foo bar とすると, 以下のように script 経由でコマンドを実行します.

# macOS の場合
script -q /dev/null foo bar
# Linux の場合はこうっぽい
script --return -qfc /dev/null 'foo bar'

script は本来は対話的なコマンドの実行を記録するためのコマンドで, 通常は記録の出力先ファイルを指定するところを, ここでは記録は必要ないので /dev/null に捨てています.

重要なのは script 経由でコマンドを実行すると, たとえ script の出力が pipe などであったとしても, コマンド自身は TTY 端末に出力していると勘違いするような形で実行されるという点です. 実際, Node.js で判別するコードを使って試してみると, 次のような結果が得られます.

$ script -q /dev/null node -e 'console.log(process.stdout.isTTY)' | cat
true

ちなみにこの方法は昔色々ぐぐっていたところ bs-loader の PR で使われているのから見つけました.

さて元の話題に戻って, ls でやってみましょう.

script -q /dev/null ls | pbcopy
LICENSE     README.md   bin     coverage    jest.config.js  lib     node_modules    package.json    yarn-error.log  yarn.lock

ババーン.


ところで色付けたくないですか? ls には -G オプションがあり, これが有効な場合はファイル / ディレクトリや権限の違いなどがわかる形で色付けして出力してくれます. というわけでやってみましょう.

script -q /dev/null ls -G | pbcopy
LICENSE        README.md      [34mbin[39;49m[0m            [34mcoverage[39;49m[0m       jest.config.js [34mlib[39;49m[0m            [34mnode_modules[39;49m[0m   package.json   yarn-error.log yarn.lock

残念ながら, なんか [34m とか出てしまっています. これは ANSI escape code というやつで, この場合は要するに出力に色を付けるためのものが, 生のまま見えてしまっている形です.

これを HTML に変換してみましょう. 変換のためのツールは色々あるようですが, ちょっと調べた感じでは ansi2html というのが良い感じでした.

適当にインストールして,

pip install ansi2html

以下のように pbcopy の前に挟みます. -i は HTML の style をインラインの属性として出力するオプションで, -s solarized はカラースキーマを指定しています. 詳しくはansi2html --help を見ましょう.

script -q /dev/null ls -G | ansi2html -i -s solarized | pbcopy

結果はこんな感じ. 無事色がついてめでたいですね.

LICENSE        README.md      bin            coverage       jest.config.js lib            node_modules   package.json   yarn-error.log yarn.lock