[ターミナル]fzfを使った自作インタラクティブアプリを作ってみよう!〜git addを快適に〜

こんにちは、平野です。

皆様におかれましては、快適なターミナルでのCUI生活を送っておられますでしょうか? 今回はfzfを使った、便利なコマンドを自作する手順について紹介してみたいと思います。

fzfにはいくつかの便利で強力なコマンドが公式に用意されていますが、今回はそれらを使わず、fzfのバイナリファイル1つを核としてコマンドを自作する方法を紹介します。
もちろん用意されたコマンドもすごく便利なんですが、自分でカスタマイズできると、あれやこれやいじれて楽しいです。

fzfとは

fzfは大量の選択肢の中から、検索を使って目的の項目を簡単に選択できるツールです。 今回の記事はfzfや類似のツールを使ったことがあるという前提で詳細は省きますが、めちゃくちゃ便利なので使ったことがないという方はこれを機会にインストールしてみて欲しいです。

fzf (GitHub)

一応説明を書いておくと、fzfを起動すると以下の画像のようになり、左の候補の中から文字列検索で絞りつつ、上下で選択できるというツールです。

文章や画像だけでは分かりづらいので、試しに使って頂くのが一番良いかと思います。1

ミニマルに自作アプリを作る

fzfが提供する機能は基本的には「標準入力のうちユーザが選択した1行のみを出力する」という機能です。 非常に単純ですが、余計な処理がないぶん様々なコマンドをパイプで繋ぐことによって広い用途に使うことができます。

また、コアとなる動作は単純ですが細かいカスタマイズが可能なので、色々いじって行くと非常に洗練されたアプリケーションに仕上げることも可能です。

fzfのインストール

fzfのインストール手順はGitHubに記載された手順で行えば良いのですが、今回はfzfをミニマルに使用するというコンセプトでやっていくので、ダウンロードページからバイナリをダウンロードしてきます。

fzfの非常に気軽な所は、1つのバイナリファイルがあれば全て動作する所です。 パスの通ったところにfzfファイルを配置すればインストール完了です。

git addするアプリを作る

サンプルとして、gitで変更のあったファイルからaddしたいファイルだけを選んでgit addを実行するアプリを作成してみます。

git status -sの利用

まずはgit addの対象となるファイル一覧の取得をします。

git status -s

でリポジトリ内で対象となるファイル一覧と、その状態を一覧で取得できます。 addするファイルはここにあるもののうちの一部でしょうから、これを選択対象の全体として使用します。

表示例は以下のようなものになります。

fzfに入力する

fzfは標準入力を受け取りますから以下のようにします。

git status -s | fzf

上記コマンドだけを実行すると以下のようにfzfが起動します。

標準入力を改行区切りで選択肢としていることが分かります。 色が消えていますが、そこはあとで何とかします。

この状態でどれか一つを選択すれば、その選択した行が標準出力として出力されます。 この標準出力を次のgit addコマンドに渡してあげれば良いですね。

選択項目に対してgit addを実行

git add コマンドの基本的な使い方は

git add <ファイルパス>

ですので、前の出力を引数として使うのにはxargsを使えばいいですね。

git status -s | fzf | xargs git add

さて、これでめでたしとなれば良いのですが、そうではありません。 git status -sの出力は、最初にそのファイルの状態があり、次にファイルパスが表示されます。 ですから、当然fzfが出す標準出力もそのまま、ファイル状態とスペースが含まれています。

 M .tmux.conf

つまりgit addに渡す前に、不要部分を除去する必要があるわけです。

しかしこれは特に大きな問題ではありません。 これは単純に標準出力として出されたものですので、2番目の要素を取り出すということでawkを使えば良いだけの話です。

git status -s | fzf | awk '{print $2}' | xargs git add

以上でコア部分は完成です。
入出力が標準入力・標準出力であることから任意のコマンドで処理することができ、非常に柔軟に使うことができる感じが伝わるでしょうか?

さて毎回これを打つのは大変なのでaliasでgaddとしておきます。

alias gadd="git status -s | fzf | awk '{print \$2}' | xargs git add"

これで、大量のファイルの中から1ファイルだけgit addしたいという場合に快適に操作することができるようになりました。

さらに便利に

上記はfzfを使った最低限のアプリの例でしたので、まだ実用に耐えられるものとは言えません。 しかし以下のような簡単な改良をするだけで、実用できる立派なアプリケーションに近づけることができます。

aliasではなく、シェルスクリプトにする

まず改良とは直接関係ありませんが、カスタマイズをしていく準備として、aliasではなくシェルスクリプトファイルとして記述していきます。 また、fzfの出力をより柔軟に扱うためにxargsをやめて、一度変数に格納する形に変えます。

function gadd() {
    local selected
    selected=$(git status -s | fzf | awk '{print $2}')
    if [[ -n "$selected" ]]; then
        git add $selected
    fi
}

fzfが途中で中断された際の安全のために、$selectedの中身があるかも確認するようにしています。

シェルスクリプトへの移行は完了です。 このスクリプトファイルをsourceすればコマンドが使えます。

複数選択を可能に

ファイルを複数選択できるようにします。 これはもっとも重要な改良です。 というか、これがないとさすがにまともに使えないですね。

fzfで複数選択を有効にする方法は簡単で、-mオプションを使えばOKです。

ただし、fzfで複数選択をした場合、選択した要素は改行コードで区切られて標準出力に送られます。 ですから、git addの引数として使用する前に改行をスペースに変換する必要があります。

function gadd() {
    local selected
    selected=$(git status -s | fzf -m | awk '{print $2}')
    if [[ -n "$selected" ]]; then
        selected=$(tr '\n' ' ' <<< "$selected")
        git add $selected
    fi
}

git status -sの色付けを有効にする

git status -sを普通に実行した場合、その出力には色がつきます。 人間がものを識別するときに色という情報は非常に強力で、私なんかはgit status -sの出力は色がないとどういう状態なのかわかりません。 なのでgaddの中でもやはり色をつけたいです。

git status -sは、パイプで次のコマンドに出力を渡した場合色が消えてしまいます。

これはgitコマンドが、「コマンドにパイプするなら色なしの方が利用しやすいでしょ」と配慮してくれているから2なのですが、これを抑制するにはunbufferというコマンドを使います。
このコマンドは以下の記事を参照させて頂きました。

less にパイプしても色が消えないようにする方法

これがないとfzfでいい感じのアプリが作れないので、影の功労者とも言うべき存在です。 Macでは

brew install expect

とすればexpectパッケージをインストールできますので、あとはPATHを通すだけです。

unbufferは後続するコマンドに、「あなたの出力はコンソール画面へ出力されますよ」という情報を送るので、パイプで次のコマンドに繋ぐ場合でもエスケープシーケンスが抑制されなくなります。

次に色付きの出力を受けるfzf側ですが、色などのエスケープシーケンスを解釈する--ansiオプションが用意されているので、これを利用します。 これを設定しないと、エスケープシーケンスも全てただの文字列として処理されます。 これによってスクリプトは次のようになります。

function gadd() {
    local selected
    selected=$(unbuffer git status -s | fzf -m --ansi | awk '{print $2}')
    if [[ -n "$selected" ]]; then
        selected=$(tr '\n' ' ' <<< "$selected")
        git add $selected
    fi
}

これで色付きのステータス一覧を見ながらファイルを選ぶことができ、グッと使いやすいアプリになりました。

プレビューにdiffを表示する

最後にさらにリッチ感をだすカスタマイズとして、プレビューを出してみます。 これは人によって蛇足と感じるかも知れませんが、私はプレビュー出すの好きです。

プレビューの使い方も簡単で、

fzf --preview="任意のコマンド"

とすればコマンドの出力結果がプレビューとして表示されます。 また、現在選択している(候補となっている)文字列を{}で呼び出すことができます。 よって、選択要素としてファイルパスが並んでいるならば

fzf --preview="git diff {}"

とすればgit diffの内容をプレビューできます。

今回の例では選択要素はファイルの状態も含めた文字列ですから、やはりawkでファイルパスだけを取り除けばOKです。 またgit diff--colorをつけることでカラー出力が可能3ですので、こちらも設定します。

function gadd() {
    local selected
    selected=$(unbuffer git status -s | fzf -m --ansi --preview="echo {} | awk '{print \$2}' | xargs git diff --color" | awk '{print $2}')
    if [[ -n "$selected" ]]; then
        selected=$(tr '\n' ' ' <<< "$selected")
        git add $selected
    fi
}

ちょっとコマンドが長くて分かりにくくなってしまいましたね。 previewのコマンドはちょっと凝ったことをやろうとすると長くなってしまうので、その場合はプレビュー用のシェルスクリプトを別に用意してそれを実行すると良いです。

完成

最後に、正しくgit addできたことが確認できないのも困るので一言echoを出すようにして一先ず完成です。

function gadd() {
    local selected
    selected=$(unbuffer git status -s | fzf -m --ansi --preview="echo {} | awk '{print \$2}' | xargs git diff --color" | awk '{print $2}')
    if [[ -n "$selected" ]]; then
        selected=$(tr '\n' ' ' <<< "$selected")
        git add $selected
        echo "Completed: git add $selected"
    fi
}

簡単な割にはそこそこ洗練されたアプリができたと思います。 まだまだ改善すべき点はありますが、これだけでも十分実用できるレベルではないでしょうか?

何が便利なのか

今回作成したgaddコマンドですが、個人的に何が便利なのか改めて考えてみます。

まずは、1コマンドから出発して直感的な順序で操作できるところです。
「これからgit addします」と宣言すると、「では候補はこちらです」と一覧を出して貰えて、そこから「コレとコレ」という具合に決定すればaddまで完了する。 この流れがとても自然なのです。 「git addしよう」と思って最初にやることがgit status -sというのは思っているよりも頭の負担になっている気がします。

あとは、一覧を見てそこから選ぶという安心感というのもあります。
git status -sで一覧を表示し、その表示自体から選択することができると、「漏れなく対象のものを選べている」という感覚を感じることができるかと思います。 またファイルを選ぶ時にも1ボタンで選択することができるので、選ぶという行為に集中することができるのも非常に大きいです。
さらにプレビューがあると目的のファイルが選べていることがよりはっきり意識できる気がします。 プレビューは全てを見る必要はなく、変更箇所の一部が見えて、誤ったファイルを選択しているわけではない、ということがわかるだけでも効果大です。

終わりに

fzfを使ってgit status -sの出力からgit addを行うアプリケーションを作成してみました。 「コマンドAで対象となる一覧を表示し、コマンドBでその一部を引数にした処理を実行」という流れを行える非常に直感的なアプリが作れました。

ここまでの要領がわかれば、他にも色々なアプリが作れることが想像できるかと思います。
例えばディレクトリの履歴から移動するアプリなんかは練習にも良いですし、何より実用性がハンパじゃないです。 もちろん既存のツールなんかもありますが、自作してみてはいかがでしょうか?

fzfは標準入力と標準出力をインターフェイスとして汎用的に使用できるよう設計されているので、発想次第で様々な要件を満たすアプリが作れるかと思います。 また今回は全く触れませんでしたが、ENTERで選択を決定するだけでなく、特定のキーを押すと別の挙動をさせたりなどのカスタマイズも可能です。
まだまだいろんな機能がありますので、また別の機会にご紹介できればと思います。


  1. 作者の方が「out dating」と言っていますが、gifアニメもあります。 
  2. 私は以前「catが色を消している」と勘違いしていましたが、catは悪くないです。 
  3. もちろんunbufferでも良いです。色付けのオプションがあればそれを、なければunbufferを使いましょう。