パイプライン処理とは
GUIは非常に直感的です。はじめて使うアプリであっても、なんとなくそれなりに動かせてしまうという点で、優れたインターフェイスと言えます。しかし効率を突き詰めると、軍配が上がるのはGUIよりもCLIでしょう。本連載の読者であれば、UnixライクなOSのCLIが持つパワーについては当然ご存知かと思います。
とはいえ、古典的なUnixコマンドの多くは、単体ではそれほど強力なものではありません。というのも、ひとつひとつのコマンドはシンプルに、特定の用途においてのみ上手く動作するよう設計されていることがほとんどだからです。こうしたコマンド群に無限のシナジーを与えるのが「パイプライン処理」です。標準入出力を通じて複数のコマンドを直列に繋げることで、複雑な処理をインスタントに組み立てることができるパイプラインは、まさにUnix哲学の体現であり、CLIの真髄はここにあると言ってもよいでしょう(個人の感想です)。これが、Unixコマンドはレゴブロックである、などと言われる所以でもあります。
しかし残念ながら、こうしたCLIのパワフルさと比較すると、我々人類の能力は少々劣ると言わざるをえません。例えば、複雑な正規表現でコマンドの出力をフィルタしようとしても上手く動かず、何度もコマンド履歴を呼び出しては、少しばかりコマンドを書き換えて試行錯誤を繰り返す……そんな経験はありませんか?
そんな貴方にお勧めするのが、リアルタイムに実行結果を確認できるパイプライン構築ツール「Pipr」です。
Piprとは
Piprとは、入力したパイプラインの実行結果をリアルタイムに確認しながら組み立てられるツールです。キーボードから入力したパイプラインを逐次評価し、結果をプレビューしてくれるため、複雑な正規表現を組み立てるような作業が非常に楽になります。まずは動作の様子を見てみてください。何をやっているか、一目で理解できると思います。
Piprのインストール
残念ながら、Piprは現時点でUbuntu向けのパッケージが用意されていません。そのためcargoを利用してインストールするか、自前でソースからビルドする必要があります。幸いDockerコンテナ内でビルドし、成果物だけを取り出すスクリプトが用意されているため、この方法でビルドするのが簡単でしょう。まずUbuntuにDockerをインストールします。
リポジトリからソースコードをcloneし、build_docker.shを実行します。このスクリプトは内部でdockerコマンドを呼び出しているのですが、docker.ioパッケージからDockerをインストールした場合、Dockerデーモンとの通信にはroot権限が必要となるため、ここではsudoをつけてスクリプトを実行しています。
ビルドが成功すると、./target/x86_64-unknown-linux-musl/release/以下にpiprという実行バイナリが生成されています。これを/usr/local/binにコピーしておきましょう。これでpiprコマンドをパスなしで呼び出せるようになります。
なお詳しくは後述しますが、piprはBubblewrapに依存しています。最近のUbuntuデスクトップであればデフォルトでインストール済みとなっていますが、もしもインストールされていない場合は、以下のコマンドでbubblewrapパッケージをインストールしておいてください。
Piprの基本的な使い方
piprコマンドを実行すると、以下の画面が表示されます。
Commandと書かれているボックス内に、実行したいパイプラインを入力してください。「Autoeval」と表示されている場合、何かキーを押す度にパイプラインが逐次、自動的に評価され、その出力がOutputに表示されます。ここでは例として、/etc/apt/sources.listをcatで表示し、その出力をsedに渡して、URLの「jp.」を削除しています[1]。なお当然ですが、入力にはTab補完も有効です。
ESCキーを押すと、Piprは終了し、その時入力したパイプラインが標準出力されます。ちなみにPipr上からは直接ファイルを書き換えたり、作成したりといったことが行えません。そのためsedでファイルを書き換えたいような場合は、組み立てたパイプラインをコピペし、端末上で改めて実行する必要があります。少々不便な気はしますが、その理由は改善策とあわせて後述します。
ヘルプの表示
Pipr上から、実行したいコマンドのヘルプやmanページを直接呼び出すこともできます。オプションを忘れてしまったような場合も、パイプラインの構築を中断せずにmanに当たることができます。別の端末を開く必要もありません。調べたいコマンドの上にカーソルを置いて、F5キーを押します。ヘルプビューアーをどうやって開くかを聞かれますので、「m」キー(man)もしくは「h」キー(--helpオプションをlessにパイプ)のどちらかを入力してください。どちらもqキーで終了すると、Piprの画面に戻ります。これも後述しますが、ヘルプビューアーとして別途infoコマンドなどを定義することも可能です
パイプラインの履歴とブックマーク
Piprには様々な入力補完機能が用意されています。まずPiprは入力したパイプラインの履歴を記憶しているため、一度入力したパイプラインを再利用しやすくなっています。履歴はシェル同様、Ctrl+P/Nキーで呼び出すことができます。またF4キーを押すと、履歴の一覧を表示できます。
履歴はデフォルトで直近の500件が記憶されます。しかし履歴とは、言ってみれば開きっぱなしのブラウザのタブのようなものです。何かのきっかけで失わないよう、本当によく使うパイプラインはしっかりブックマークしておくべきでしょう。パイプラインを入力した状態でCtrl+Sキーを押すと、そのパイプラインをブックマークします。Ctrl+Bキーを押すとブックマークの一覧が表示され、履歴と同様に選択して呼び出すことができます。
履歴とブックマークはそれぞれ、「~/.config/pipr/historyとbookmarks」というテキストファイルに保存されています。後述する設定ファイルもここに保存されますので、バックアップを取りたい場合や他のマシンへ設定を移行したい場合は、「~/.config/pipr」ディレクトリをコピーするとよいでしょう。
スニペット
ブックマークはパイプライン全体を再利用するための機能ですが、パイプラインはその特徴として「あるコマンドに繋げるパーツ」を再利用したいこともよくあります。そこで役立つのがスニペットです。例えば「| awk '{print $1}'
」のような、ありがちなフィルタ例をスニペットとして登録すると便利です。Ctrl+Vキーを押すと、登録されているスニペットの一覧が表示されます。表示されているシンボルキーを押すことで、カーソル位置にスニペットを挿入します。スニペットはブックマークとは異なり、設定ファイルで定義します(これも後述します)。
パイプラインのキャッシュ
今時であれば、curlでWebサーバーのAPIを叩き、レスポンスのJSONに対してjqでフィルタをかけて……といったパイプラインを作ることもよくあると思います。そしてjqの記法は複雑で人類には早すぎるため、ここでもPiprが大活躍します。
Piprでは、デフォルトでコマンドのタイムアウトが2秒に制限されているため、応答に時間がかかるコマンドは失敗してしまいます。タイムアウト時間を伸ばすことは可能ですが、キーを入力する度に逐次評価が走り、応答を待たされるのでは効率が悪すぎますし、何よりサーバーに対してエコではありません。特にAPIの呼び出し回数に制限があったり、回数で課金されるようなサービスではなおさらでしょう。
そこでPiprにはパイプラインの実行結果をキャッシュする機能が用意されています。パイプライン中の任意の「|
」記号にカーソルを合わせ、F7キーを押してください。コマンド入力位置に「Caching」と表示され、カーソル位置より前にあるコマンドの実行結果がキャッシュされます。このキャッシュは、|
より前のコマンドが変更されない限り保持されます。
危険なコマンドを入力してしまったら?
cat
やgrep
などであればそれほど問題はありませんが、ファイルを作成したり削除したりといった副作用のあるコマンドをPiprに入力するとどうなるでしょうか? 特にデフォルトではキーをひとつ入力する度に逐次評価が行われますから、例えば
と入力したら、「h」「ho」「hog」「hoge」という4つのファイルが作成されることにならないでしょうか? rm
コマンドなんて、タイプするだけでヤバいことにならないでしょうか?
Piprの挙動としてはYesですが、実際にファイルが作成されたり、削除されたりすることはありません。というのも、Piprはデフォルトでisolationモード、すなわち内部的にBubblewrapを利用し、隔離空間内でプロセスを実行するモードで起動しています[2]。Piprから見えるルートファイルシステムは読み込み専用でマウントされているため、Pipr内でファイルを変更することはできません[3]。冒頭で述べた「Pipr上からはファイルの作成や書き換えが行えず、改めて端末上から実行する必要がある」理由がこれです。
Bubblewrapについては第686回でも解説していますので、あわせてご参照ください。またisolationモードは無効にすることも可能ですが、危険すぎるため本記事では紹介しません。
Piprのカスタマイズ
Piprの設定ファイルは、~/.config/pipr/pipr.tomlです。設定を変更したい場合は、このファイルをテキストエディタで編集してください。主な設定項目について以下で紹介します。
逐次評価の禁止
繰り返しますが、Piprはデフォルトで、何かキーが押される度にパイプラインを逐次評価します。そのためコマンド入力中は、オプションや引数が不完全な状態でコマンドが何度も実行されてしまい、Output欄にはエラーメッセージやヘルプが表示されてしまいます。これを好ましくないと思う人もいるでしょう。設定ファイルの「autoeval_mode_default
」をfalse
にすると、明示的にEnterキーが押されるまで、Piprはパイプラインの評価を行わなくなります。
シンタックスハイライト
デフォルトではシンタックスハイライトが有効となっており、入力されたコマンドのオプションや引数を、文脈に応じてハイライト表示します。しかし使っているターミナルによっては、正しくハイライト表示されない場合もあるでしょう。筆者が試したところ、macOSのターミナルからSSH越しに実行した場合、オプション部分が水色で反転表示されてしまい、非常に見づらくなりました。そのような場合は「highlighting_enabled
」をfalse
にして、ハイライトを無効にしてください。
スニペットの登録
前述のスニペットは、pipr.toml内で定義します。「[snippets]」以下に「シンボルキー = '入力したいスニペット'
」を入力してください。デフォルトではsキーに、sed
で検索にマッチした削除するスニペットが定義されています。なおスニペット内に「||
(パイプ2本)」を入力すると、スニペット挿入後、その場所にカーソルが移動します。そのためCtrl+V → Sと入力することで、パイプラインの末尾にスニペットを挿入した上で、削除したい文言の正規表現を直ちに入力できるようになるというわけです。
例えば以下のように「a = 〜
」の行を追加すると、Ctrl+V → Aの入力で、awk
で任意のカラムを取り出すスニペットを挿入することができます。この例では、$
の直後に自動的にカーソルが移動しますので、取り出したいカラムの番号を入力してください。
ヘルプビューアーの開き方
「[help_viewers]」以下では、コマンドのヘルプビューアーを開く手段を定義できます。デフォルトでは前述の通り、man
と--help
オプションが定義されています。定義方法はスニペットと同様、シンボルキーと実行したいコマンドを=
で結びます。またコマンド中の「??
」は、ヘルプが呼び出された際に、カーソルが当たっているコマンドに置換されます。例えば以下のように「i = 〜
」の行を追加すると、F5 → Iキーでコマンドのinfoを呼び出すことができるようになります。
終了フック
「finish_hook
」では、Piprの終了時に実行するコマンドを設定できます。Piprは終了時、finish_hook
に指定されたコマンドの標準入力へ、組み立てたパイプラインの内容をパイプで渡します。例えばデスクトップ環境でPiprを実行しているのであれば、xclip
コマンドにパイプすれば、組み立てたパイプラインをクリップボードにコピーできます。パイプラインを別の端末で再利用しやすくなり、非常に便利です。この処理はデフォルトではコメントアウトされていますので、有効にしたい場合は以下のように行頭の「#
」を削除してください。
Piprをシェルと統合して便利に使う
このように便利なPiprですが、使ってみると不満な点もあります。それは「組み立てたパイプラインを実行するには、一度コピペする必要がある」という点です。デスクトップ環境であれば、前述のxclip
を使うことで多少は手間を軽減できますが、SSH越しに利用しているような場合はそれもままなりません。
この問題を改善するためのスクリプトが、ソースコードのshell_integration
ディレクトリ内に用意されています。fishとzsh用のスクリプトがありますが、今回はzshを使う方法を紹介します。まずzshをインストールし、シェルをbashから変更します。
続いて、「pipr/shell_integration/pipr_hotkey.zsh」を読み込みます。毎回手動で読み込むのが面倒であれば、~/.zshrcの最後に以下のコマンドを記述するか、あるいはpipr_hotkey.zshの中身をまるごと転記してしまってもよいでしょう。
このファイルの内容は以下のようになっています。
Piprをdefaultオプションつきで起動し、LBUFFER変数の内容をパイプライン入力欄のデフォルト値として読み込んでいます。この変数はzshの特殊な変数で、現在のカーソル位置よりも左側に入力されている文字列が格納されています。すなわち、コマンドプロンプトの後に入力中のコマンドです。つまりシェル上で入力中のコマンドを、そのままPiprに引き継いでいます。
それと同時に、Piprに--out-file
オプションを指定しています。Piprは終了時に、入力したパイプラインを標準出力へ出力しますが、--out-file
オプションが指定されている場合は、そのファイルに出力します。そしてこのファイルの内容をLBUFFER変数に書き戻すことで、シェルに入力中のコマンドを更新しています。そしてこの一連の処理を_pipr_expand_widget
というシェル関数にまとめ、ESC+Aキーにバインドしているのです。
このスクリプトを読み込んだ状態で、何かコマンドを入力してからESC+Aキーを押してみましょう。Piprが起動しますので、パイプラインを完成された後に、ESCキーでPiprを終了してみてください。
毎回sedコマンドの正規表現に苦戦したり、awsコマンドやjqコマンドでのJSONのパースに試行錯誤している(筆者の)ような方は、是非Piprを試してみてください。