エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

dotfilesのこだわりを晒す

Unit4の永山です。 dotfiles弄りを趣味にしています。

世にdotfilesを題材とした記事は数多く存在していますがその大半は「dotfilesを作ってみた」「こうやって管理しています」などの表層的な部分の紹介に留まり、その奥にあるべき細部のこだわりや個人の思想にまで踏み込んだ記事は数えるほどしかありません。

そこで、本記事では私のdotfilesを題材にその各構成要素についてオススメ, TIPS, こだわりに分類し、可能な限り詳細に紹介します。

github.com

本記事は筆者の関心の都合上、Zshに関する項目に大きく比重を置いています。ご承知おきください。

dotfilesとは

*nixのソフトウェアには、慣例的に.から始まるファイルに設定を記述するものが多く存在しています。

  • Bash: .bashrc, .bash_profile など
  • Git: .gitconfig
  • Vim: .vimrc
  • etc...

dotfilesはこれらの.から始まる設定ファイル全般を指し、また、これらの設定ファイルを管理するリポジトリをdotfilesリポジトリと呼びます。 一般に、単にdotfilesといった場合はこのdotfilesリポジトリを意味することが多いです (以下、dotfilesリポジトリを単にdotfilesと表記します)。

余談ですが、dotfilesを育てたり剪定したりする感覚が盆栽の育成に近いことから、dotfilesを弄ることを指して「dotfiles盆栽」と表現されることがあります。

dotfilesを作成することの利点

dotfilesを作成することには以下のような利点があります。

  • 設定ファイルの変更履歴を残せる
  • 複数のマシンで同じ開発体験が得られるようになる
  • 新規マシンのセットアップが簡単になる

記事の構成

以下の節では筆者のdotfilesをもとに様々な項目について詳細に解説します *1

各項目には性質に応じて以下のいずれかのプリフィクスをつけています。

  • [オススメ]: 従うことでなんらかの利益が得られる項目
  • [TIPS]: ささいな利益が得られる項目、または小ネタ
  • [こだわり]: 筆者の思想やこだわりが強く反映された項目

Zsh編

ログインシェルにはZshを用いています。 以下ではZshの設定に関して紹介します。

[オススメ] プラグインの管理にZinitを使う

Zshのプラグインマネージャには Antigenzplug がありますが、基本的に Zinit を用いるのがオススメです。

github.com

Zinitは実行速度に優れたプラグインマネージャです。 また、プラグインの非同期読み込みに対応しており、後述するように適切にプラグインの読み込みを最適化することでZshの起動時間を非常に短くできます。

さらに特筆すべき機能として、GitHub Releasesからのプラグインのインストールに対応しています。 そのため、GitHub Releasesでプリビルトバイナリを配布しているGoやRust製のCLIツールを他のZshプラグインと同じように扱えます。

zinit wait lucid light-mode as'program' from'gh-r' for \
    pick'ripgrep*/rg' @'BurntSushi/ripgrep'

Rust製のツール ripgrep をGitHub Releasesからインストールする例

Zinitを利用する利点と欠点を以下にまとめます。

  • 利点
    • 高速に動作する
    • プラグインを非同期読み込みできる
    • GitHub Releasesからのインストールに対応している
  • 欠点
    • オプションなどの指定方法がやや複雑

注釈: Zinitについて

上記の3つの利点のために、この記事ではZinitをオススメしていますが、2022年現在Zinitを取り巻く状況はやや不安定です。 その経緯についてかいつまんで説明します。

Zinit はもともと Zplugin (zdharma/zplugin (リンク先は削除済み)) という名前で開発されていましたが、2020年1月頃に Zinit (zdharma/zinit) へと改名されました。 その後も、zdharma/zinit リポジトリで開発が続けられましたが、2021年10月に作者である@psprint氏が突如Zinitのリポジトリとzdharma organizationを削除しました。

上記で紹介している zdharma-continuum/zinit は有志によって復旧された最もポピュラーなZinitのForkで、Zinitユーザのコミュニティにより現在も開発が続けられています。

現在の zdharma organizationと zdharma/zinit、およびそこにリンクが記載されている z-shell/zi (こちらもZinitのFork) は第三者により再取得されたもので元作者である@psprint氏とは無関係です*2zdharma-continuum/zinitz-shell/zi の中身はどちらもほぼ同一ですが、個人的な感情から z-shell/zi ではなく zdharma-continuum/zinit の利用をオススメしています。

また、速度や機能面に対してのこだわりが薄いのであれば zimfw/zimfw など他のプラグインマネージャの利用を視野にいれるとよいでしょう。

[オススメ] Zshプラグインは非同期読み込みする

Zinitでは、プラグインの読み込み時に wait オプションを指定することでプラグインの読み込みを非同期にできます。

プラグインの読み込みを非同期にすると、Zsh初期化時のオーバヘッドが小さくなり、起動の高速化に繋がります。

zinit wait lucid blockf light-mode for \
    @'zsh-users/zsh-autosuggestions' \
    @'zsh-users/zsh-completions' \
    @'zdharma-continuum/fast-syntax-highlighting'

Zshプラグインを非同期読み込みする例

ただし、シェルプロンプトは同期的に読み込む必要があります。 これは、同期的に読み込まなければプロンプトの初回のレンダリングが行えないためです。

  • シェルプロンプト: 同期的に読み込む
  • それ以外: 非同期的に読み込む

[オススメ] .zshrc に書く設定は最小限に留め、その他を遅延させる

筆者の .zshrc を見ると、.zshrc ではZinitの初期化やプロンプトの読み込みなどのみを行っていることがわかると思います。

github.com

その他のプラグインの読み込みや、エイリアスの登録などは .zshrc.lazy という別ファイルに分割し、その読み込みを遅延させることで起動時間をさらに短くしています。

# $ZDOTDIR/.zshrc
zinit wait lucid light-mode as'null' \
    atinit'source "$ZDOTDIR/.zshrc.lazy"' \
    for 'zdharma-continuum/null'

.zshrc.lazy を非同期で読み込んでいる例

上記で用いているzdharma-continuum/null は遅延実行を実現するために用意されている空のリポジトリです。

では、.zshrc.lazy の遅延読み込みの有無でどの程度起動速度に差がでるのか見てみましょう。

まずは遅延読み込みを無効化した場合の例です。

# $ZDOTDIR/.zshrc
#zinit wait lucid light-mode as'null' \
#    atinit'source "$ZDOTDIR/.zshrc.lazy"' \
#    for 'zdharma-continuum/null'
source "$ZDOTDIR/.zshrc.lazy"
$ for i in {1..5}; do time zsh -ic exit; done
zsh -ic exit  0.07s user 0.04s system 93% cpu 0.111 total
zsh -ic exit  0.07s user 0.04s system 93% cpu 0.110 total
zsh -ic exit  0.07s user 0.04s system 94% cpu 0.107 total
zsh -ic exit  0.07s user 0.04s system 93% cpu 0.108 total
zsh -ic exit  0.07s user 0.04s system 94% cpu 0.113 total

.zshrc.lazyを遅延読み込みしなかった場合のZshの起動時間

続いて.zshrc.lazy を遅延読み込みした場合です。

$ for i in {1..5}; do time zsh -ic exit; done
zsh -ic exit  0.02s user 0.01s system 84% cpu 0.033 total
zsh -ic exit  0.02s user 0.01s system 82% cpu 0.038 total
zsh -ic exit  0.02s user 0.01s system 84% cpu 0.033 total
zsh -ic exit  0.02s user 0.01s system 85% cpu 0.033 total
zsh -ic exit  0.02s user 0.02s system 86% cpu 0.037 total

.zshrc.lazyを遅延読み込みした場合のZshの起動時間

起動時間が約1/3になっています。

Shellは1日に何度も起動することになるアプリケーションであるため、起動速度の差は開発体験に直結してきます。

[オススメ] BSD系CLIツールをGNU系に置き換える (macOS)

macOSにデフォルトでインストールされているCLIツールには歴史的経緯からBSD系のものが採用されています。

BSD系のCLIツールはコマンドラインオプションや一部の動作がGNU系と異なっており、しばしば混乱の原因になります。

一方で、現代のシェルスクリプトは多くの場合CIやDockerコンテナ内などのGNU系CLIツールがインストールされているLinux上で動作します。

ローカル環境もGNU系CLIツールで統一することで、開発時のいらぬストレスを軽減できます。

# GNU系のCLIツールをインストールする
$ brew install coreutils findutils gnu-sed grep

# .zshrc.lazy
case "$OSTYPE" in
    darwin*)
        (( ${+commands[gdate]} )) && alias date='gdate'
        (( ${+commands[gls]} )) && alias ls='gls'
        (( ${+commands[gmkdir]} )) && alias mkdir='gmkdir'
        (( ${+commands[gcp]} )) && alias cp='gcp'
        (( ${+commands[gmv]} )) && alias mv='gmv'
        (( ${+commands[grm]} )) && alias rm='grm'
        (( ${+commands[gdu]} )) && alias du='gdu'
        (( ${+commands[ghead]} )) && alias head='ghead'
        (( ${+commands[gtail]} )) && alias tail='gtail'
        (( ${+commands[gsed]} )) && alias sed='gsed'
        (( ${+commands[ggrep]} )) && alias grep='ggrep'
        (( ${+commands[gfind]} )) && alias find='gfind'
        (( ${+commands[gdirname]} )) && alias dirname='gdirname'
        (( ${+commands[gxargs]} )) && alias xargs='gxargs'
    ;;
esac

基本的なコマンドをGNU系ツールに置き換える例

[TIPS] 不要なコマンドをhistoryから除外する

Zshでは、zshaddhistory というhookを定義することでhistoryへのコマンドの登録/除外を制御できます。

zshaddhistory の終了ステータスが0の場合、直前のコマンドはhistoryに保存され、0以外の場合はhistoryから除外されます。

cdls などは多くの場合historyに保存しても役に立たないので除外しています。

zshaddhistory() {
    local line="${1%%$'\n'}"
    [[ ! "$line" =~ "^(cd|jj?|lazygit|la|ll|ls|rm|rmdir)($| )" ]]
}

zshaddhistoryを用いて一部のコマンドをhistoryから除外する例

[こだわり] $ZDOTDIR を変更する

通常 .zshrc などはホームディレクトリに配置されたものが読み込まれますが、$ZDOTDIR という環境変数を変更することで任意のディレクトリに配置できるようになります。

$ZDOTDIR を変更する場合は .zshenv というファイルをホームディレクトリに作成します。

# ~/.zshenv
export XDG_CONFIG_HOME="$HOME/.config"
export ZDOTDIR="$XDG_CONFIG_HOME/zsh" # .zshrc や .zprofile が ~/.config/zsh から読み込まれるようになる

$ZDOTDIR を変更する例

筆者はXDG Base Directory Specificationに従う形で$ZDOTDIRを設定しています。

XDG Base Directory Specificationは設定ファイルやキャッシュファイルを配置するディレクトリの構造に関する規約です*3。 XDG Base Directory Specificationに従うことでホームディレクトリを整理でき、また、設定ファイルなどを一箇所に集約できるため管理がしやすくなります。

Zshプラグイン編

以下では使用しているZshプラグインのうちいくつかについて紹介・解説します。

現在、筆者は以下のようなZshプラグイン(またはCLIツール)をZinitを用いてインストールして利用しています*4

Zinitを用いて導入しているZshプラグイン・CLIツール

[オススメ] zeno.zsh

zeno.zshDeno製のZshプラグインです。

github.com

zenn.dev

zeno.zshは 略語展開FZFを用いた補完 という2つの機能を提供します。

1. 略語展開

略語展開は自動展開されるaliasのような機能です (Vimmerにとってはiabと言った方が通じやすいかもしれせん)。

# $XDG_CONFIG_HOME/zeno/config.yml
snippets:
  - name: g
    keyword: g
    snippet: git

  - name: yyyymmdd
    keyword: yyyymmdd
    snippet: date "+%Y%m%d"
    evaluate: true # snippetをシェルスクリプトとして解釈し、その出力を展開する
    global: true # 行頭以外でも展開する (Zshのglobal alias相当)
$ g<スペース>
   ↓ 即座に展開される
$ git

$ git tag yyyymmdd<エンター>
   ↓ 展開され、実行される
$ git tag 20220401

zeno.zshの略語展開の例

略語展開では、通常のaliasとは異なり、展開後のコマンドがhistoryに保存されるという利点があります。

略語展開のみであれば、olets/zsh-abbr などのZshプラグインでも実現できますが、zeno.zshではさらに展開に必要なコンテキストを指定することができます。

snippets:
  - name: git c
    keyword: c
    snippet: commit
    context:
      lbuffer: ^git\s # gitコマンドを入力しているときのみ展開する

  - name: git push -f
    keyword: -f
    snippet: --force-with-lease
    context:
      lbuffer: ^git(\s+\S+)*\s+push\s # git pushを入力しているときのみ展開する
$ git c<スペース>
   ↓ 展開される
$ git commit

$ git push -f<エンター>
   ↓ 展開され、実行される
$ git push --force-with-lease

$ echo c<スペース>
   ↓ 展開されない
$ echo c

コンテキストを指定した略語展開の例

コンテキストを指定できることで、global alias自体の利便性・有用性を保ったままグローバル名前空間の汚染を避けることができます *5

また、Git aliasとは異なり、historyに展開後のコマンドが保存される点でも優れています。

2. FZFを用いた補完

FZF はGo製のファジーファインダーです。

zeno.zshでは任意のコマンドに対する、FZFを用いたインタラクティブな補完をカスタマイズできます *6。 また、デフォルトでgit-addなどのいくつかのGitコマンドなどに対する補完が組み込まれているため、ある程度設定不要でその恩恵を受けることができます。

zeno.zsh組み込みの git-add のファジー補完

zeno.zshは実行にDenoのランタイムを要求しますが、作業を効率化できるため非常にオススメできるZshプラグインです。

筆者のその他のzeno.zshの設定については以下で確認できます。

https://github.com/NagayamaRyoga/dotfiles/blob/main/config/zeno/config.ymlgithub.com

※ zeno.zshは zsh-users/zsh-syntax-highlighting との相性が悪いため、代わりに zdharma-continuum/fast-syntax-highlighting を使用することをオススメします。

[オススメ] zsh-replace-multiple-dots

momo-lab/zsh-replace-multiple-dotsは入力された ...../.. へと展開するだけのシンプルなプラグインです。

$ cd ...
   ↓ 展開される
$ cd ../..

zsh-replace-multiple-dots の展開の例

github.com

qiita.com

上述のzeno.zshと組み合わせることで、...<エンター> と入力するだけで cd ../.. に展開されるようにしています *7

snippets:
  - name: ..
    keyword: ..
    snippet: cd ..

  - name: ../..
    keyword: ../..
    snippet: cd ../..

  - name: ../../..
    keyword: ../../..
    snippet: cd ../../..

zsh-replace-multiple-dots と zeno.zsh を組み合わせる場合の zeno/config.yml の例

また、go test ./... などと入力したときに go test ./../.. と展開されるのを防ぐため、以下のようにZLEウィジェットを再登録することで go コマンドのみ ... の展開を防いでいます。

replace_multiple_dots_exclude_go() {
    if [[ "$LBUFFER" =~ '^go ' ]]; then
        zle self-insert # goコマンドのみ通常通り `.` を入力する
    else
        zle .replace_multiple_dots # goコマンド以外では `...` を `../..` に展開
    fi
}

zle -N .replace_multiple_dots replace_multiple_dots
zle -N replace_multiple_dots replace_multiple_dots_exclude_go

go コマンドのみ zsh-replace-multiple-dots を無効化する例

[オススメ] コードスニペットの管理に navi を利用する

特に業務ではDBへのアクセスコマンドやプロジェクトの起動コマンドなどの雑多で小規模なコマンドを何度も入力することになります。

一方でそれらをすべて正確に、長期間に渡って記憶するのは現実的ではありませんが、忘れるたびにプロジェクトのREADMEをgrepするのも手間です。

このような場合に history からの検索を用いる方もいると思いますが、私はこのようなコードスニペットの管理に denisidoro/navi を使用することをオススメします。

github.com

naviはCLIのチートシート管理ツールです。 独自形式のテキストファイルでコードスニペットのチートシートを管理でき、FZFなどのファジーファインダーを用いてそれらの検索や実行が行なえます。

naviによって管理されたチートシートの検索の様子

naviを用いてコードスニペットを管理することで、history の上限超えなどによって以前に実行したコマンドが蒸発することを気にしなくてよくなる他、作成したチートシートを小規模な個人用のドキュメントとしても活用できます。

Zsh小ネタ編

以下ではその他のZshに関する小ネタについて紹介します。

[TIPS] PATH環境変数ではなく path を使う

Bashでは特定のディレクトリにPATHを通すために環境変数 PATH を使用しますが、Zshではこれに加えて配列変数 path も用意されています。

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

$ echo ${(F)path}
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

Zshの配列変数 path

path を用いると、"特定のディレクトリが存在しているときのみ、そのディレクトリを PATH に追加する" といったことが簡単に、読みやすく書けます。

# `$GOPATH/bin` が存在する場合のみ `PATH` に追加する
path=(
    "$GOPATH/bin"(N-/)
    "$path[@]"
)

配列変数 path へディレクトリを追加する例

また、FPATH に対応した変数として fpath も用意されています。

[TIPS] docker コマンドを上書きする

GitはGit aliasや git-foo という名前の実行可能ファイルに対してPATHを通すことでカスタムサブコマンドを定義することができますが、Dockerではそのようなことはできません。

そこで、私は以下のように docker コマンドを上書きしています。

docker() {
    if [ "$1" = "compose" ] || ! command -v "docker-$1" >/dev/null; then
        command docker "${@:1}" # 通常通りdockerコマンドを呼び出す
    else
        "docker-$1" "${@:2}" # docker-foo というコマンドが存在するときはそちらを起動する
    fi
}

カスタムサブコマンドを実現するための上書きされた docker コマンド

# docker clean
# ExitedなDockerプロセスをすべて削除する
docker-clean() {
    command docker ps -aqf status=exited | xargs -r docker rm --
}
# docker cleani
# UntaggedなDockerイメージをすべて削除する
docker-cleani() {
    command docker images -qf dangling=true | xargs -r docker rmi --
}
# docker rm (上書き)
# 引数なしで docker rm したときはプロセスをFZFで選択して削除する
docker-rm() {
    if [ "$#" -eq 0 ]; then
        command docker ps -a | fzf --exit-0 --multi --header-lines=1 | awk '{ print $1 }' | xargs -r docker rm --
    else
        command docker rm "$@"
    fi
}
# docker rmi (上書き)
# 引数なしで docker rmi したときはイメージをFZFで選択して削除する
docker-rmi() {
    if [ "$#" -eq 0 ]; then
        command docker images | fzf --exit-0 --multi --header-lines=1 | awk '{ print $3 }' | xargs -r docker rmi --
    else
        command docker rmi "$@"
    fi
}

カスタム docker サブコマンドの例

これによって、「組み込みコマンドのときは docker foo」「カスタムコマンドのときは docker-bar」 などの呼び分けを気にする必要がなくなります。

Git編

[オススメ] リポジトリをghqで管理する

特に業務ではたくさんのGitリポジトリで作業をする必要が発生しがちです。 そのような場合に x-motemen/ghq を用いることで、リポジトリの管理を効率化できます。

github.com

ghq はGitリポジトリ*8を管理するためのシンプルなツールです。

# $(ghq root)/github.com/NagayamaRyoga/dotfiles に
# https://github.com/NagayamaRyoga/dotfiles をクローンする
$ ghq get NagayamaRyoga/dotfiles

$ ls -al "$(ghq root)/github.com/NagayamaRyoga/dotfiles"
total 40
drwxr-xr-x  11 ryoga.nagayama  staff   352 Mar 11 18:36 .
drwxr-xr-x   3 ryoga.nagayama  staff    96 Mar 11 18:36 ..
-rw-r--r--   1 ryoga.nagayama  staff   416 Mar 11 18:36 .editorconfig
drwxr-xr-x  13 ryoga.nagayama  staff   416 Mar 11 18:36 .git
...

ghq を用いてGitHubリポジトリをクローンする例

# ghqで管理されているすべてのリポジトリを一括で更新する
$ ghq list | ghq get --update --parallel

ghq で管理されているすべてのリポジトリを一括で更新する例

ghq の基本的な利用方法については以下のドキュメントが詳しいです。

github.com

ghq とTmuxやZLEと合わせて利用することでリポジトリの切り替えなどを劇的に効率化できます。 ghq の活用例については [こだわり] セッションをリポジトリごとに分ける の節で紹介します。

[TIPS] XDG Base Directory Specificationに従う

通常Gitは ~/.gitconfig をグローバルな設定ファイルとして用いますが、GitはXDG Base Directory Specificationに準拠しているため、$XDG_CONFIG_HOME/git/config も同様にグローバルな設定ファイルとして利用できます。

$ mv ~/.gitconfig "$XDG_CONFIG_HOME/git/config"

また、$XDG_CONFIG_HOME/git/ignore をグローバルな .gitignore として利用できるため、git config core.excludesfile '~/.gitignore_global' などとわざわざ明示的に指定する必要はありません。

 # $XDG_CONFIG_HOME/git/config
-[core]
-    excludesfile = ~/.gitignore_global

[こだわり] .gitconfig を分割する

$XDG_CONFIG_HOME/git/config にすべての設定を記述するとごちゃつきがちです。

トピックごとに設定を分割することで設定の編集や確認がしやすくなります。

[include]
    path = conf.d/alias.conf
    path = conf.d/user.conf
    path = conf.d/delta.conf
    path = conf.d/flow.conf
    path = conf.d/forgit.conf
    path = conf.d/ghq.conf
    path = conf.d/gist.conf
    path = conf.d/github.conf
    path = conf.d/local.conf

$XDG_CONFIG_HOME/git/config を分割する例

include.path はその設定の書かれているファイルの相対パスを指定できるため、この場合 $XDG_CONFIG_HOME/git/conf.d/*.conf を読み込んでいることになります。

[オススメ] commit.verbosetrue を設定する

git config commit.verbose true とすることで git commit に常に -v/--verbose オプションを渡しているのと同様の動作になります。

git-scm.com

[オススメ] merge.conflictStylediff3 を設定する

git config merge.conflictStyle diff3 とすることでマージコンフリクトが発生した際に、マージ元・マージ先のみでなくマージベース (両ブランチの共通の先祖) もコンフリクトマーカーに追加されるようになります。

merge.conflictStylediff3 を指定した場合のマージコンフリクト

これによって、マージ時に「以前はどのような状態であったか」がわかりやすくなります。

git-scm.com

Tmux編

[こだわり] XDG Base Directory Specificationに従う

Tmux 3.2以降では $XDG_CONFIG_HOME/tmux/tmux.conf を設定ファイルとして利用できます。

$ mv ~/.tmux.conf "$XDG_CONFIG_HOME/tmux/tmux.conf"

[こだわり] セッションをリポジトリごとに分ける

筆者はシェルを使う際は基本的にほぼすべての作業をTmuxセッションの中で行っていますが、その際、Gitリポジトリごとにセッションを立てるようにしています。

セッションを分けることで、リポジトリごとに作業状況や起動しているプロセスを維持できるため、別リポジトリでの作業からの復帰が簡単になります。

Tmuxセッションの起動・アタッチ・切り替えには以下のようなZLEウィジェットを使用しています。

# Gitリポジトリを列挙する
widget::ghq::source() {
    local session color icon green="\e[32m" blue="\e[34m" reset="\e[m" checked="\uf631" unchecked="\uf630"
    local sessions=($(tmux list-sessions -F "#S" 2>/dev/null))

    ghq list | sort | while read -r repo; do
        session="${repo//[:. ]/-}"
        color="$blue"
        icon="$unchecked"
        if (( ${+sessions[(r)$session]} )); then
            color="$green"
            icon="$checked"
        fi
        printf "$color$icon %s$reset\n" "$repo"
    done
}
# GitリポジトリをFZFで選択する
widget::ghq::select() {
    local root="$(ghq root)"
    widget::ghq::source | fzf --exit-0 --ansi --preview="fzf-preview-git ${(q)root}/{+2}" --preview-window="right:60%" | cut -d' ' -f2-
}
# FZFで選択されたGitリポジトリにTmuxセッションを立てる
widget::ghq::session() {
    local selected="$(widget::ghq::select)"
    if [ -z "$selected" ]; then
        return
    fi

    local repo_dir="$(ghq list --exact --full-path "$selected")"
    local session_name="${selected//[:. ]/-}"

    if [ -z "$TMUX" ]; then
        # Tmuxの外にいる場合はセッションにアタッチする
        BUFFER="tmux new-session -A -s ${(q)session_name} -c ${(q)repo_dir}"
        zle accept-line
    elif [ "$(tmux display-message -p "#S")" = "$session_name" ] && [ "$PWD" != "$repo_dir" ]; then
        # 選択されたGitリポジトリのセッションにすでにアタッチしている場合はGitリポジトリのルートディレクトリに移動する
        BUFFER="cd ${(q)repo_dir}"
        zle accept-line
    else
        # 別のTmuxセッションにいる場合はセッションを切り替える
        tmux new-session -d -s "$session_name" -c "$repo_dir" 2>/dev/null
        tmux switch-client -t "$session_name"
    fi
    zle -R -c # refresh screen
}
zle -N widget::ghq::session

# C-g で呼び出せるようにする
bindkey "^G" widget::ghq::session

FZFで選択したGitリポジトリにTmuxセッションを立ち上げるZLEウィジェット

このZLEウィジェットは、ghqで管理されているGitリポジトリをFZFを用いて選択し、

  1. Gitリポジトリに対応するTmuxセッションが存在しない場合は新しくセッションを起動し、アタッチする
  2. Gitリポジトリに対応するTmuxセッションが存在する場合はそのセッションにアタッチする
  3. すでにGitリポジトリに対応するTmuxセッションにアタッチしている場合はGitリポジトリのルートディレクトリに移動する

という動作を行います。

`widget::ghq::select` ウィジェットによってGitリポジトリを選択している様子

上図の緑色の行はGitリポジトリに対応するTmuxセッションがすでに立ち上がっていることを表しています。

[オススメ] lazygitを呼び出すキーバインドを設定する

jesseduffield/lazygit はTUIのGitクライアントで、CLIからグラフィカルにGitを操作できます。 類似のツールにtiggituiがありますが、デフォルトのレイアウトやキーバインディングが好みなためlazygitを主に利用しています。

github.com

以下のようにTmuxのキーバインディングを設定することで、Tmuxのpopup windowを用いて任意の操作中にlazygitを呼び出せるようにしています。

# $XDG_CONFIG_HOME/tmux/tmux.conf
bind g popup -w90% -h90% -E lazygit # (prefix) gでlazygitを起動する

Tmuxのpopup windowでlazygitを起動している様子

popup windowを用いることで、Tmuxのペインを分割している場合でも全画面に近いサイズでlazygitを起動できます。 また、時間のかかるコマンドを実行している最中に手早くファイルのステージングやコミットなどの操作を行うことができるため、非常に重宝しています。

mac編

[オススメ] macOSの設定を defaults コマンドでコードとして管理する

macOSでは defaults コマンドを用いて一部の設定をコードとして管理できます。 設定をGit管理することで、異なるmacOSマシン間の設定の差異を小さくできます。

# セットアップスクリプト

# Dock
defaults write com.apple.dock orientation right
defaults write com.apple.dock autohide -bool false
defaults write com.apple.dock tilesize -int 50
defaults write com.apple.dock magnification -bool false
defaults write com.apple.dock show-recents -bool false

# Mission Control
defaults write com.apple.dock wvous-br-corner -int 4 # 画面右下にカーソルを移動するとデスクトップを表示する
defaults write com.apple.dock mru-spaces -bool false # ワークスペースの順序をMRUにしない

killall Dock

defaults コマンドを使用して設定を更新する例

[オススメ] スニペット入力ツールを使用する

業務・趣味に限らず、同じテキストを頻繁に入力しなければならない場面は多数存在します。 例えばユーザIDやメールアドレスは日常的に入力する機会があるでしょう。

そのような文字列の入力にはスニペット入力ツールを使用すると良いでしょう *9。 筆者の場合、業務で使用しているmacOSではDashのスニペット機能を、趣味のmacOSにはEspansoを使用しています。

スニペット    テキスト
--------    ----------------------
;id         NagayamaRyoga
;mail       XXXXXX@example.com
;gh         github.com
;m3id       ryoga-nagayama
;oha        おはようございます
;owa        終わります。お疲れさまでした
;review     U4コードレビュー担当

業務PCに登録しているスニペットの例*10

その他

[こだわり] ホームディレクトリの掃除

前節まででXDG Base Directory Specificationに従うように.zshrc, .gitconfig, .tmux.confなどのファイルをホームディレクトリから移動してきました。

筆者はホームディレクトリに置かれるファイルの数は少なければ少ないほどよいと考えています*11。 というわけで、以下ではホームディレクトリに作成されるファイルを掃除していきます。

~/.zsh_history

.zsh_historyHISTFILE 環境変数で保存先を変更できます。

# $ZDOTDIR/.zshrc
export HISTFILE="$XDG_STATE_HOME/zsh_history"

XDG_STATE_HOME (~/.local/state) は近年XDG Base Directory Specificationに追加されたディレクトリで、ログやヒストリーファイルを保存するのに適したディレクトリです。

specifications.freedesktop.org

~/.node_repl_history, ~/.sqlite_history, ~/.mysql_history, ~/.psql_history

これらはそれぞれ NODE_REPL_HISTORY, SQLITE_HISTORY, MYSQL_HISTORY, PSQL_HISTORY 環境変数を設定することで保存先を変更できます。

# $ZDOTDIR/.zshrc.lazy
export NODE_REPL_HISTORY="$XDG_STATE_HOME/node_history"
export SQLITE_HISTORY="$XDG_STATE_HOME/sqlite_history"
export MYSQL_HISTFILE="$XDG_STATE_HOME/mysql_history"
export PSQL_HISTORY="$XDG_STATE_HOME/psql_history"

~/.irb_history

.irb_historyIRBRC 環境変数で指定される設定ファイル内で保存先を変更できます。

# $ZDOTDIR/.zshrc.lazy
export IRBRC="$XDG_CONFIG_HOME/irb/irbrc"
# $IRBRC
xdg_state_home = ENV['XDG_STATE_HOME'] || "#{ENV['HOME']}/.local/state"
IRB.conf[:HISTORY_FILE] = "#{xdg_state_home}/irb_history"

~/go

Goの作業ディレクトリは GOPATH 環境変数で変更できます。

# $ZDOTDIR/.zshrc.lazy
export GOPATH="$XDG_DATA_HOME/go"

ここで挙げたソフトウェア以外についてもArchWikiのサポートセクションにXDG Base Directory Specificationのサポート状況や、非準拠のものにおける回避方法などがまとめられています。

まとめ

筆者のdotfilesをもとに、オススメの設定やこだわりについて紹介しました。 今後このようなこだわりについて語る記事が増えてくれると嬉しいです。

We are hiring!

エムスリーでは強いこだわりを持ったエンジニアを募集しています。 興味を持たれた方は下記よりお問い合わせください。

jobs.m3.com

*1:筆者の dotfiles は macOS および Ubuntu 20.04 (on WSL2) を前提としていますが、CLIに関する大半の項目は他の環境でも適用できるはずです。

*2:https://github.com/z-shell/zi/discussions/138

*3:現在ではfish shellGitNeovimをはじめとする様々なソフトウェアがXDG Base Specificationに準拠しています。

*4:私はcli/cliなどのZshプラグインではないいくつかのCLIツールもZinitを用いてインストールしています。これは、Homebrewなどのパッケージマネージャを経由せずGitHub Releasesからバイナリを直接ダウンロードすることでOS間の差分を少なくできるためです。

*5:様々なdotfilesを覗くと alias gs='git status' などのGitサブコマンドをaliasに登録しているのをよく見かけますが、Ghost Script (gs) をはじめとするコマンドと衝突するためあまり好みではありません。

*6:FZFを用いた補完を実現しているZshプラグインとして chitoku-k/fzf-zsh-completions などがありますが、zeno.zshはYAMLを用いて任意のコマンドに対する補完を記述できるという点でよりカスタマイズ性に優れています。

*7:Zshの setopt AUTO_CD を使う手もありますが、好みの問題で有効化していません。

*8:正確にはSubversionなどのGit以外のVCSにも対応しています。

*9:スニペット入力ツールにパスワードを登録するのは避けましょう。

*10:"U4コードレビュー担当"というのはSlack上でコードレビュー担当者を割り当てるためのカスタムレスポンスのトリガーです(参考)。

*11:ホームディレクトリはいわば部屋における床面です。物を散らかすのであれば棚の中や机の上がふさわしいでしょう。