深酒とお昼寝で忘れる

深酒とお昼寝で忘れる

素面でも意図したことを忘れがちなしらふいとさんは、忘れる前に何かしら書き残せたらとても満足のようです

Zsh で実行に失敗したコマンドを履歴に残さない テイク2

あらすじ:zsh で(例えば、間違ってタイプして)実行に失敗したコマンドを履歴に残さないように 以前の記事 で設定しました。ところが使っているうちに一つ気に入らない振る舞いをしている点が見つかります。この度、久しぶりに設定ファイルに手を入れて改良してみることにしました。

さて何が問題だったかというと、例えば

echo $fpath | tr ' ' '\n'

というコマンドが、履歴ファイルには

echo $fpath | tr ' ' '
'

という風に保存されてしまう点です……(\nが評価されてしまっている)。これはひどい

以前の実装では、実行するコマンド(${1%%$'\n'})を一時変数(_LASTCMD)に保存し、実行直後に _LASTCMD にいくつかの処理を施し、終了ステータスが 0 ならば履歴に書き込んでいました。この『いくつかの処理』で echo ${_LASTCMD} としている箇所があったのが問題です。これは具体的には zsh のオプションで設定できる hist_reduce_blanks という機能*1を自前で実装しているところに登場します。Shell expansion に精通していたら回避できるような気もするのですが、諦めて別の手段で解決します。そもそも zsh に元来備わっている機能を自前で実装する必要があるアプローチは筋が悪い。

ということで今までは

『実行前にコマンドを一時保存 → コマンドの文字列にいくつかの処理 → コマンドを履歴ファイルに保存』

としていたところを

『実行コマンドを履歴ファイルに保存(いくつかの処理は zsh におまかせ)→ 終了ステータスを見て不要なら履歴ファイルから消去』

とします。

こちら を参考にしました。下記のような設定を .zshrc などに記入すれば良いはずです。*2

__update_history() {
  local last_status="$?"

  local HISTFILE=~/.zsh_history
  fc -W
  if [[ ${last_status} -ne 0 ]]; then
    ed -s ${HISTFILE} <<EOF >/dev/null
d
w
q
EOF
  fi
}
precmd_functions+=(__update_history)

実に 2 年越しの改良ですが(今のところ)概ね満足しています。*3

*1:コマンドの文字列から余分なスペースを省いてヒストリに保存する機能です。

*2:正直なところ fc -W/-R の挙動がいまいち良く分かってないのですが、正しく動いているように見えます。

*3:実際には諸事情でもう少し違った実装にしていますが、基本方針はこの通りです。

YouCompleteMe (YCM) を用いた快適な C++ のコードリーディング

はじめに

それなりに大きなコードベースをエディタで読むときには、カーソルの下の変数・関数や、クラスやメンバ関数の宣言・定義などを確認したくなるものです。以前 書いたように rg などを用いて(findgrep の代替だと思ってもらって OK です)読んでも良いのですが、何も考えずに一発で正しい場所に飛べると思考が途切れないので助かります。

比較的よくあるニーズだからか、Vim では(例えば)関数の定義されているファイル名と位置を記録した tags と呼ばれるファイルを用意すると、CTRL-] ショートカットでカーソルの下の関数名の定義に飛ぶことができます(CTRL-T で飛び元に戻る)。 tags ファイルの生成には Exuberant Ctags(または後継の Universal Ctags)、Cscope または GNU GLOBAL などを用いれば良く、「C 言語のプロジェクトならば」基本的にストレスなく目的が達成できます。*1 ところがタイトルにあるように問題は C++ 言語で、上記のアプローチはの複雑さに対してやや力が足りません(理由については man ctagsBUGSman cscopeNotices セクションをどうぞ)。

そこで、今のところ最も良い解決策だと思っているのは 悪名高い(最も嫌われているって…笑 *2YouCompleteMe(略して YCM) という Vimプラグインを使うことです。 YCM は Vim 用のコード補完エンジンで、コードリーディングに役立つ機能も備えています。C 言語ファミリー(C、C++、Objective C、Objective C++)に関してはソースコードの解析を libclang に頼っており*3(意味解析を行ない生成した AST をメモリに置くため)高速かつ 100% 正確に宣言・定義に飛ぶことができるのです。

インストール(とアップデート)

YCM はクライアントサーバ型のプラグインでサーバが C++ で書かれており(クライアントは Python)、また最新の libclang を使うことが推奨されているため、これらのコンパイルが必要になります。個人的にはコンパイルが必要なプラグインは自前で管理したいので(この記事dein_local.toml で読み込みます)プラグインを使わずにインストールします。プラグインPLUGIN_DIR ディレクトリに保存するとしたら下記のような手順を踏みます。*4

$ cd ${PLUGIN_DIR}
$ git clone https://github.com/Valloric/YouCompleteMe
$ cd YouCompleteMe
$ git submodule update --init --recursive
$ python2 ./install.py --clang-completer

--clang-completerC言語ファミリーに対応させるためのオプションになります。*5 ちなみにアップデートする場合は、下記の様に git pull して(submodule も忘れずに)から再度コンパイルします。

$ cd ${PLUGIN_DIR}/YouCompleteMe
$ git pull origin master
$ git submodule foreach git pull origin master
$ python2 ./install.py --clang-completer

準備

libclang を使って意味解析をするにはソースコードを parse する必要があるため、コンパイルする際のフラグ情報が必要になります。最も簡単な手段は compilation database を用意することです。下記の様な選択肢があります。

  • CMake をビルドに使用する場合は configure の際に -DCMAKE_EXPORT_COMPILE_COMMANDS=ON というオプションを渡す(または CMakeLists.txtset(CMAKE_EXPORT_COMPILE_COMMANDS ON) を追加する)と良いです。
  • Bear というプログラムを用いて、ビルドコマンドを wrap する。makescons だったら $ bear make$ bear scons と実行します。

これらでフラグ情報を備えた compile_commands.json というファイルが生成されるので、プロジェクトのルートディレクトリに置けば YCM が Vim の起動時に読み込んでくれます。これで準備が整いました。

本題

ここまで来たらあとはコマンドを実行するだけです。YCM のマニュアルにあるとおり、

nnoremap <Leader>jd :YcmCompleter GoTo<CR>

とでもショートカットを設定しておけば、例えばメソッド名の上で <Leader>jd とタイプすればその定義に飛ぶことができるはずです。ちなみに GoTo は下記の候補から該当「してそうな」ものに飛んでくれます。

  • GoToInclude: #include <stdio.h> などヘッダが記述されている行で実行されたら該当するファイルに飛ぶ。
  • GoToDeclaration:カーソル下の単語の宣言に飛ぶ。
  • GoToDefinition:カーソル下の単語の定義に飛ぶ。

宣言と定義では定義が優先されます。

ちなみにこの GoToVim のタグスタックに登録「されない」ため CTRL-T で飛び元に戻ることはできません。ジャンプリストには登録されるため、CTRL-OCTRL-I で行き来することになります。

おわりに

C++ のプロジェクトにおけるコードリーディングを強力にサポートしてくれる YCM という Vimプラグインを紹介しました。コード補完と合わせて、個人的にはかかせないプラグインになっています。最後に少し触れましたがタグスタックに登録されないのが気にいらないので、アドホックな手段で解決しています。こちらについてはまたの機会に紹介したいと思います。

*1:ソースコードに変更を加えると対応する tags ファイルの情報と実際の位置がずれるため、何らかの方法で tags ファイルを更新する必要があります。

*2:セットアップに骨が折れる、100MB 超のサイズ、などなど。

*3:例えば Python では jedi を使います。

*4: Linux だったら build-essential、cmake、python-dev、python3-dev などのパッケージをインストールしておく必要があります。

*5:他にも C#、Go、TypeScript、JavaScript、Rust、Java に対応させるオプションがあるようなので、使ってみたい場合は YCM のマニュアルを見てください。