Ubuntu Weekly Recipe

第723回複雑なコマンドパイプラインを簡単に組み立てる方法

パイプライン処理とは

GUIは非常に直感的です。はじめて使うアプリであっても、なんとなくそれなりに動かせてしまうという点で、優れたインターフェイスと言えます。しかし効率を突き詰めると、軍配が上がるのはGUIよりもCLIでしょう。本連載の読者であれば、UnixライクなOSのCLIが持つパワーについては当然ご存知かと思います。

とはいえ、古典的なUnixコマンドの多くは、単体ではそれほど強力なものではありません。というのも、ひとつひとつのコマンドはシンプルに、特定の用途においてのみ上手く動作するよう設計されていることがほとんどだからです。こうしたコマンド群に無限のシナジーを与えるのが「パイプライン処理」です。標準入出力を通じて複数のコマンドを直列に繋げることで、複雑な処理をインスタントに組み立てることができるパイプラインは、まさにUnix哲学の体現であり、CLIの真髄はここにあると言ってもよいでしょう(個人の感想です⁠⁠。これが、Unixコマンドはレゴブロックである、などと言われる所以でもあります。

しかし残念ながら、こうしたCLIのパワフルさと比較すると、我々人類の能力は少々劣ると言わざるをえません。例えば、複雑な正規表現でコマンドの出力をフィルタしようとしても上手く動かず、何度もコマンド履歴を呼び出しては、少しばかりコマンドを書き換えて試行錯誤を繰り返す……そんな経験はありませんか?

そんな貴方にお勧めするのが、リアルタイムに実行結果を確認できるパイプライン構築ツールPiprです。

Piprとは

Piprとは、入力したパイプラインの実行結果をリアルタイムに確認しながら組み立てられるツールです。キーボードから入力したパイプラインを逐次評価し、結果をプレビューしてくれるため、複雑な正規表現を組み立てるような作業が非常に楽になります。まずは動作の様子を見てみてください。何をやっているか、一目で理解できると思います。

動画1 Piprの実行例。ここではlsコマンド、sortコマンド、headコマンドをそれぞれパイプで繋いでみた。コマンドの入力にあわせてOutputが変化するのに注目してほしい

Piprのインストール

残念ながら、Piprは現時点でUbuntu向けのパッケージが用意されていません。そのためcargoを利用してインストールするか、自前でソースからビルドする必要があります。幸いDockerコンテナ内でビルドし、成果物だけを取り出すスクリプトが用意されているため、この方法でビルドするのが簡単でしょう。まずUbuntuにDockerをインストールします。

$ sudo apt install docker.io

リポジトリからソースコードをcloneし、build_docker.shを実行します。このスクリプトは内部でdockerコマンドを呼び出しているのですが、docker.ioパッケージからDockerをインストールした場合、Dockerデーモンとの通信にはroot権限が必要となるため、ここではsudoをつけてスクリプトを実行しています。

$ git clone https://github.com/elkowar/pipr.git
$ cd pipr
$ sudo ./build_docker.sh

ビルドが成功すると、./target/x86_64-unknown-linux-musl/release/以下にpiprという実行バイナリが生成されています。これを/usr/local/binにコピーしておきましょう。これでpiprコマンドをパスなしで呼び出せるようになります。

$ sudo cp ./target/x86_64-unknown-linux-musl/release/pipr /usr/local/bin

なお詳しくは後述しますが、piprはBubblewrapに依存しています。最近のUbuntuデスクトップであればデフォルトでインストール済みとなっていますが、もしもインストールされていない場合は、以下のコマンドでbubblewrapパッケージをインストールしておいてください。

$ sudo apt install bubblewrap 

Piprの基本的な使い方

piprコマンドを実行すると、以下の画面が表示されます。

図1 Piprの基本的な画面。ここで対話的にパイプラインを入力し、結果を確認しながら編集するのが基本となる
図1

Commandと書かれているボックス内に、実行したいパイプラインを入力してください。⁠Autoeval」と表示されている場合、何かキーを押す度にパイプラインが逐次、自動的に評価され、その出力がOutputに表示されます。ここでは例として、/etc/apt/sources.listをcatで表示し、その出力をsedに渡して、URLの「jp.」を削除しています[1]⁠。なお当然ですが、入力にはTab補完も有効です。

動画2 sedで文字列を削除する例。こうしたシンプルな例であれば問題ないが、複雑な正規表現を組み立てたい場合に威力を発揮するのがわかると思う

ESCキーを押すと、Piprは終了し、その時入力したパイプラインが標準出力されます。ちなみにPipr上からは直接ファイルを書き換えたり、作成したりといったことが行えません。そのためsedでファイルを書き換えたいような場合は、組み立てたパイプラインをコピペし、端末上で改めて実行する必要があります。少々不便な気はしますが、その理由は改善策とあわせて後述します。

ヘルプの表示

Pipr上から、実行したいコマンドのヘルプやmanページを直接呼び出すこともできます。オプションを忘れてしまったような場合も、パイプラインの構築を中断せずにmanに当たることができます。別の端末を開く必要もありません。調べたいコマンドの上にカーソルを置いて、F5キーを押します。ヘルプビューアーをどうやって開くかを聞かれますので、⁠m」キー(man)もしくは「h」キー(--helpオプションをlessにパイプ)のどちらかを入力してください。どちらもqキーで終了すると、Piprの画面に戻ります。これも後述しますが、ヘルプビューアーとして別途infoコマンドなどを定義することも可能です

動画3 Piprからmanやhelpを呼び出す例

パイプラインの履歴とブックマーク

Piprには様々な入力補完機能が用意されています。まずPiprは入力したパイプラインの履歴を記憶しているため、一度入力したパイプラインを再利用しやすくなっています。履歴はシェル同様、Ctrl+P/Nキーで呼び出すことができます。またF4キーを押すと、履歴の一覧を表示できます。

図2 履歴表示画面。カーソルキーで履歴を選択し、Enterで入力することができる。なぜかここではCtrl+P/Nが使えないのは謎と言える
図2

履歴はデフォルトで直近の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」と表示され、カーソル位置より前にあるコマンドの実行結果がキャッシュされます。このキャッシュは、|より前のコマンドが変更されない限り保持されます。

図3 awsコマンドの出力をjqでフィルタする例。JSONのフィルタはawsコマンド自身のqueryオプションでも可能だが、Piprがレスポンスをキャッシュできることを考慮すると、パイプでjqに繋いだ方が試行錯誤しやすいと言える
図3

危険なコマンドを入力してしまったら?

catgrepなどであればそれほど問題はありませんが、ファイルを作成したり削除したりといった副作用のあるコマンドをPiprに入力するとどうなるでしょうか? 特にデフォルトではキーをひとつ入力する度に逐次評価が行われますから、例えば

$ touch hoge

と入力したら、⁠h」⁠ho」⁠hog」⁠hoge」という4つのファイルが作成されることにならないでしょうか? rmコマンドなんて、タイプするだけでヤバいことにならないでしょうか?

Piprの挙動としてはYesですが、実際にファイルが作成されたり、削除されたりすることはありません。というのも、Piprはデフォルトでisolationモード、すなわち内部的にBubblewrapを利用し、隔離空間内でプロセスを実行するモードで起動しています[2]⁠。Piprから見えるルートファイルシステムは読み込み専用でマウントされているため、Pipr内でファイルを変更することはできません[3]⁠。冒頭で述べた「Pipr上からはファイルの作成や書き換えが行えず、改めて端末上から実行する必要がある」理由がこれです。

図4 PID 1として、bwrapコマンドが--ro-bindで起動されていることがわかる
図4

Bubblewrapについては第686回でも解説していますので、あわせてご参照ください。またisolationモードは無効にすることも可能ですが、危険すぎるため本記事では紹介しません。

Piprのカスタマイズ

Piprの設定ファイルは、~/.config/pipr/pipr.tomlです。設定を変更したい場合は、このファイルをテキストエディタで編集してください。主な設定項目について以下で紹介します。

逐次評価の禁止

繰り返しますが、Piprはデフォルトで、何かキーが押される度にパイプラインを逐次評価します。そのためコマンド入力中は、オプションや引数が不完全な状態でコマンドが何度も実行されてしまい、Output欄にはエラーメッセージやヘルプが表示されてしまいます。これを好ましくないと思う人もいるでしょう。設定ファイルのautoeval_mode_defaultfalseにすると、明示的にEnterキーが押されるまで、Piprはパイプラインの評価を行わなくなります。

autoeval_mode_default = true
↓
autoeval_mode_default = false

シンタックスハイライト

デフォルトではシンタックスハイライトが有効となっており、入力されたコマンドのオプションや引数を、文脈に応じてハイライト表示します。しかし使っているターミナルによっては、正しくハイライト表示されない場合もあるでしょう。筆者が試したところ、macOSのターミナルからSSH越しに実行した場合、オプション部分が水色で反転表示されてしまい、非常に見づらくなりました。そのような場合はhighlighting_enabledfalseにして、ハイライトを無効にしてください。

highlighting_enabled = true
↓
highlighting_enabled = false

スニペットの登録

前述のスニペットは、pipr.toml内で定義します。⁠[snippets]」以下にシンボルキー = '入力したいスニペット'を入力してください。デフォルトではsキーに、sedで検索にマッチした削除するスニペットが定義されています。なおスニペット内に||(パイプ2本)」を入力すると、スニペット挿入後、その場所にカーソルが移動します。そのためCtrl+V → Sと入力することで、パイプラインの末尾にスニペットを挿入した上で、削除したい文言の正規表現を直ちに入力できるようになるというわけです。

例えば以下のようにa = 〜の行を追加すると、Ctrl+V → Aの入力で、awkで任意のカラムを取り出すスニペットを挿入することができます。この例では、$の直後に自動的にカーソルが移動しますので、取り出したいカラムの番号を入力してください。

[snippets]
s = " | sed -r 's/||//g'"
a = " | awk '{print $||}'"

ヘルプビューアーの開き方

「[help_viewers]」以下では、コマンドのヘルプビューアーを開く手段を定義できます。デフォルトでは前述の通り、man--helpオプションが定義されています。定義方法はスニペットと同様、シンボルキーと実行したいコマンドを=で結びます。またコマンド中の??は、ヘルプが呼び出された際に、カーソルが当たっているコマンドに置換されます。例えば以下のようにi = 〜の行を追加すると、F5 → Iキーでコマンドのinfoを呼び出すことができるようになります。

[help_viewers]
'm' = "man ??"
'h' = "?? --help | less"
'i' = "info ??"

終了フック

finish_hookでは、Piprの終了時に実行するコマンドを設定できます。Piprは終了時、finish_hookに指定されたコマンドの標準入力へ、組み立てたパイプラインの内容をパイプで渡します。例えばデスクトップ環境でPiprを実行しているのであれば、xclipコマンドにパイプすれば、組み立てたパイプラインをクリップボードにコピーできます。パイプラインを別の端末で再利用しやすくなり、非常に便利です。この処理はデフォルトではコメントアウトされていますので、有効にしたい場合は以下のように行頭の#を削除してください。

# finish_hook = "xclip -selection clipboard -in"
↓
finish_hook = "xclip -selection clipboard -in"

Piprをシェルと統合して便利に使う

このように便利なPiprですが、使ってみると不満な点もあります。それは「組み立てたパイプラインを実行するには、一度コピペする必要がある」という点です。デスクトップ環境であれば、前述のxclipを使うことで多少は手間を軽減できますが、SSH越しに利用しているような場合はそれもままなりません。

この問題を改善するためのスクリプトが、ソースコードのshell_integrationディレクトリ内に用意されています。fishとzsh用のスクリプトがありますが、今回はzshを使う方法を紹介します。まずzshをインストールし、シェルをbashから変更します。

$ sudo apt install zsh
$ chsh -s /usr/bin/zsh

続いて、⁠pipr/shell_integration/pipr_hotkey.zsh」を読み込みます。毎回手動で読み込むのが面倒であれば、~/.zshrcの最後に以下のコマンドを記述するか、あるいはpipr_hotkey.zshの中身をまるごと転記してしまってもよいでしょう。

$ source pipr/shell_integration/pipr_hotkey.zsh

このファイルの内容は以下のようになっています。

_pipr_expand_widget() {
  emulate -LR zsh
  </dev/tty pipr --out-file /tmp/pipr_out --default "$LBUFFER" >/dev/null
  LBUFFER=$(< /tmp/pipr_out)
}
zle -N _pipr_expand_widget
bindkey '\ea' _pipr_expand_widget

Piprをdefaultオプションつきで起動し、LBUFFER変数の内容をパイプライン入力欄のデフォルト値として読み込んでいます。この変数はzshの特殊な変数で、現在のカーソル位置よりも左側に入力されている文字列が格納されています。すなわち、コマンドプロンプトの後に入力中のコマンドです。つまりシェル上で入力中のコマンドを、そのままPiprに引き継いでいます。

それと同時に、Piprに--out-fileオプションを指定しています。Piprは終了時に、入力したパイプラインを標準出力へ出力しますが、--out-fileオプションが指定されている場合は、そのファイルに出力します。そしてこのファイルの内容をLBUFFER変数に書き戻すことで、シェルに入力中のコマンドを更新しています。そしてこの一連の処理を_pipr_expand_widgetというシェル関数にまとめ、ESC+Aキーにバインドしているのです。

このスクリプトを読み込んだ状態で、何かコマンドを入力してからESC+Aキーを押してみましょう。Piprが起動しますので、パイプラインを完成された後に、ESCキーでPiprを終了してみてください。

動画4 zshの統合を有効にした例。コマンド入力をPiprをシームレスに行き来できて便利

毎回sedコマンドの正規表現に苦戦したり、awsコマンドやjqコマンドでのJSONのパースに試行錯誤している(筆者の)ような方は、是非Piprを試してみてください。

おすすめ記事

記事・ニュース一覧