Dotfiles の整頓とバージョン管理：XDG Base Directory Specification と YADM の活用ガイド

# はじめに
初めまして、Qiita での初投稿となります。よろしくお願いします 😸

新しい Macbook Pro を買って、環境設定している最中です。
そこで、元の開発環境は「ホームディレクトリ（以降 `$HOME`）には dotfiles が多すぎてめちゃくちゃだ！」と気づきました。Dotfiles を効果的に管理する方法について調べて、実際に試してみた結果をメモしておきたいと思います。

# 私の環境
**OS** - macOS Monterey Version 12.6.3
**Terminal** - iTerm2 Build 3.4.19
**Shell** - zsh 5.8.1 (x86_64-apple-darwin21.0)

# そもそも dotfile とは？
Dotfile（ドットファイル）とは、ファイル名がドットで始まるファイルのことを指します。通常、ユーザー固有のアプリケーション設定に使われます。例えば `.bashrc`、`.vimrc`、`.gitconfig` などが該当します。

私の $HOME にはたくさんの dotfiles があります。数えてみましょう：

```shell
$ find ~ -maxdepth 1 -name '.*' -exec basename {} ";" | wc -l
```

![count-dotfiles.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546971/920a817c-25c6-195a-18ac-84a531465ac9.jpeg)
なんと、113 個もの dotfiles が散らばっています 🙀
自分の部屋や仕事用の机は整理するように、自分の `$HOME` も整頓したほうがいいと思います。

それでは、どうやって dotfiles を整頓し、効率的に管理するかを模索していきましょう。

# Dotfiles の整頓およびバージョン管理の手順

1. `$HOME` に dotfiles を格納するためのをフォルダー作成する
2. Dotfiles をフォルダーに移動させ、ファイルの場所変更にも対応させる
3. YADM を利用して dotfiles をバージョン管理を行う

# 1. `$HOME` に dotfiles を格納するためのをフォルダー作成する

最初に、散らばっている dotfiles をフォルダーにまとめて整頓します。
では、どういう基準で dotfiles を分類すればいいですか？もちろん、各 dotfile を使用するソフトウェアによって分類してもいいものの、ここでは広く使われている仕様である「XDG Base Directory Specification」に従って分類しましょう。

## XDG Base Directory Specification
「[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)（以降 XDG 仕様）」というのは、X Window System や systemd などを生み出したプロジェクトである「freedesktop.org」が定めた、dotfiles の管理方法に関する仕様です。この仕様により、dotfiles はその使用目的に応じて異なる場所に保存されることとなりました。

この仕様は、主に 5 つの使用目的に基づいて dotfiles を分類しています：

| 使用目的 | 環境変数 | デフォルト値 | 説明 |
| --- | --- | --- | --- |
| config | `$XDG_CONFIG_HOME` | `$HOME/.config/` | 設定ファイル |
| cache | `$XDG_CACHE_HOME` | `$HOME/.cache/` | 重要でないファイル（キャッシュ） |
| data | `$XDG_DATA_HOME` | `$HOME/.local/share/` | データファイル |
| state | `$XDG_STATE_HOME` | `$HOME/.local/state/` | 状態ファイル |
| runtime | `$XDG_RUNTIME_DIR` |  | ランタイムファイルやその他のファイルオブジェクト |

`$XDG_RUNTIME_DIR` はユーザー固有のもので、今回の dotfiles を管理する上では関係ない話になるため、飛ばしましょう。興味のある方は仕様を読んでください。

Dotfiles を整理する前に、まずはそれらのフォルダーを用意しましょう。

```shell
$ mkdir -p $HOME/.config/ $HOME/.cache/ $HOME/.local/share/ $HOME/.local/state/
```

# 2. Dotfiles をフォルダーに移動させ、ファイルの場所変更にも対応させる

フォルダーを作った後は、ソフトウェアごとに dotfiles を相応するフォルダーに移動させます。
すでに XDG 仕様に対応しているソフトウェアに対しては、そのまま移動するだけでは問題ない一方、そうじゃないソフトウェアには少し工夫が必要です。たとえば、ソフトウェアが使う環境変数を調整して、dotfiles の場所変更に対応させるか、alias を作ってコマンドラインオプションで dotfiles の場所を指定するなどの工夫が求められます。
そこで、より効率的に作業を進めるようなツールを紹介します。

## xdg-ninja

XDG 忍者？忍術でも使いますか？いや違います。

> A shell script which checks your $HOME for unwanted files and directories.
>
> xdg-ninja の [README](https://github.com/b3nj5m1n/xdg-ninja/blob/main/README.md) より引用

xdg-ninja というのは、`$HOME` には不要なファイルやディレクトリを見つけて教えてくれる、すごく便利なシェルスクリプトです。

「`$HOME` には不要」？`$HOME` に保存して何が悪いですか？

> Because you wouldn't let just anyone into your $HOME
>
> 引用元：同上

**訳**：なぜなら、自分の家には誰にでも入らせるわけにはいかないからです。

ふざけた話はさておき、実際にインストールして使ってみましょう。

```shell
$ brew install --formula xdg-ninja
$ xdg-ninja
```

![telegram-cloud-photo-size-5-6138567233576744813-y.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546971/0ea85a23-9f47-720c-3b8f-4a9cdffc724c.jpeg)

画像のように、xdg-ninja さんは `$HOME` に存在する dotfiles を検出してくれます。さらに、xdg-ninja に収録されているソフトウェアには、どうすれば XDG 仕様に対応するかまでも教えてくれます。

これらを一つずつ解決していきましょう。まずは、一番よく使っているシェルから始めます。

## ZSH

私は普段 zsh を使っています。xdg-ninja さんにどう対応すればいいか聞きましょう！

![2023-03-30 21.52.40.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546971/86c6bcb5-2d1d-5517-601c-dbe4462a2a90.jpeg)
私の `$HOME` には zsh の関連ファイルは主に 3 種類があります。

A. 起動時に読み込まれるファイル（[zsh startup files](https://zsh.sourceforge.io/Intro/intro_3.html)）
\- `.zshenv`、`.zshrc`
B. コマンド履歴を記録するヒストリファイル
\- `.zsh_history`
C. 補完システムに使われているキャッシュファイル
\- `.zcompdump`

xdg-ninja の指示通りに、それぞれのファイルを対処しましょう。

### A. 起動時に読み込まれるファイル

起動時に読み込まれるファイルは XDG 仕様では設定ファイルとして分類されます。
そのため、zsh 関連の設定ファイルは `$XDG_CONFIG_HOME/zsh/` に移動させることになります。

xdg-ninja さんのアドバイスも確認しましょう。

```
[zsh]: $HOME/.zshenv

Move file to *"$HOME"/.config/zsh/.zshenv* and export the following environment variable in */etc/zsh/zshenv*:

export ZDOTDIR="$HOME"/.config/zsh
```

まずはファイルを `"$HOME"/.config/zsh/.zshenv` に移動させて、次に `/etc/zsh/zshenv` の中で `$ZDOTDIR` の環境変数も変更します。

#### `/etc/zsh/zshenv` vs `/etc/zshenv`

ここは `/etc/zsh/zshenv` と書いてあるけど、私の環境（macOS）では `/etc/zshenv` を使います。
これは `$ man zsh` で確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546971/607a9e5c-977b-a100-bdc9-082aa03b2373.png)
では `/etc/zsh/zshenv` と `/etc/zshenv` はどう違うかというと、

> The default directory for the 'global' config files in the zsh repository is simply /etc, and this is what you'll see e.g. on the Web site, but the paths can all be changed when the shell is built. Several Linux distributions use /etc/zsh. The documentation should reflect the configured paths; if your distribution's man pages don't do that they might be building it weirdly.
>
> Reddit の [/etc/zshenv vs /etc/zsh/zshenv](https://www.reddit.com/r/zsh/comments/ewzrr6/etczshenv_vs_etczshzshenv/#t1_fg5ri1j) より引用

どうやらシェルはビルド時にグローバル設定のパスを変えられるようです。いくつかの Linux ディストリビューションでは `/etc/zsh/` を使うが、私の環境では `/etc/` を使います。みなさんも確認してみてくださいね。

ちなみに xdg-ninja の開発者 b3nj5m1n さんは macOS のユーザーではないです。（本人：[I'm not a mac user](https://github.com/b3nj5m1n/xdg-ninja/issues/132#issuecomment-1304391196)）おそらくそれで対応していないでしょう。

#### `$ZDOTDIR`

zsh では `$ZDOTDIR` という環境変数が存在します。

> The directory to search for shell startup files (.zshrc, etc), if not $HOME.
>
> zsh の[ドキュメント](https://zsh.sourceforge.io/Doc/Release/Parameters.html#index-ZDOTDIR)より

`.zshrc` など、シェル起動時に読み込まれるファイルを検索するディレクトリは `$ZDOTDIR` で指定できます。

#### 手順
1. `.zshenv` に XDG 仕様の環境変数を追加する
    ```shell
    # ~/.zshenv
    export XDG_DATA_HOME=$HOME/.local/share/
    export XDG_CONFIG_HOME=$HOME/.config/
    export XDG_STATE_HOME=$HOME/.local/state/
    export XDG_CACHE_HOME=$HOME/.cache/
    ```

2. `/etc/zshenv` で `$ZDOTDIR` を変更する
   （**ここは OS によって違うから、「 [/etc/zsh/zshenv vs /etc/zshenv](#etczshzshenv-vs-etczshenv) 」を読んでから各自の判断でやってください**）
    ```shell
    $ echo "export ZDOTDIR=$HOME/.config/zsh" | sudo tee -a /etc/zshenv
    $ sudo chmod 444 /etc/zshenv # /etc/zshrc の権限を参照して設定した
    ```

3. zsh 起動時に読み込まれるファイルを `$ZDOTDIR` に移動させる
    ```shell
    $ mkdir -p ~/.config/zsh
    $ for zsh_startup_files in {.zshenv,.zprofile,.zshrc,.zlogin,.zlogout}; do mv ~/$zsh_startup_files ~/.config/zsh/; done;
    ```

`source` コマンドで変更を適用させるか、シェルを再起動しなければならないが、これらの変更は全て終わってからやりましょう。

### B. コマンド履歴を記録するヒストリファイル

A のように、ヒストリファイルの位置を指定する環境変数 `$HISTFILE` を変更して、移動させれば良いのです。

1. zsh 起動時に読み込まれるファイルに `$HISTFILE` を設定する
    通常、環境変数は `.zshenv` に設定すべきだが、どうやら Apple さんは `/etc/.zshrc` にて `$HISTFILE` を設定したようです。ファイルが読み込まれる順番を考慮して、`$XDG_CONFIG/zsh/.zshrc` に設定することにします。また、皆さんの環境は異なるかもしれないので、自身の判断で適切な場所に設定を入れてください。以下は私の history 関連の設定例です。

    ```sh
    # $XDG_CONGIG/zsh/.zshrc

    ### HISTORY ###
    export HISTFILE=$XDG_STATE_HOME/zsh/history
    (( HISTSIZE = (2 ** 31) - 1 ))   # Number of history can be saved in memory
    (( SAVEHIST = (2 ** 31) - 1 ))   # Number of history can be saved in HISTFILE
    setopt INC_APPEND_HISTORY        # Write to the history file immediastely, not when the shell exits.
    setopt SHARE_HISTORY             # Share history between all sessions.
    setopt HIST_IGNORE_DUPS          # Don't record an entry that was just recorded again.
    ### HISTORY ###
    ```

2. `$HISTFILE` を移動させる
    ```shell
    $ mkdir -p ~/.local/state
    $ mv $HISTFILE ~/.local/state
    ```

### C. 補完システムに使われているキャッシュファイル

zsh の補完システムについては[ドキュメント](https://zsh.sourceforge.io/Doc/Release/Completion-System.html)を読んでください。ここで言いたいのは、`compinit` コマンドを実行すると、`.zcompdump` というキャッシュファイルが生成されることです。さらに `-d` フラグを付けば、生成されるファイルの場所を指定できることです。

```shell
$ compinit -d some/random/path/for/cache/file
```

私はデフォルトシェル zsh の他に、zsh のフレームワークである [Oh My Zsh](https://github.com/ohmyzsh/ohmyzsh) やプラグインマネージャーである [antigen](https://github.com/zsh-users/antigen) を使っています。これらのソフトウェアは内部で `compinit` を呼び出すため、キャッシュファイルが生成されます。

ここで触れた内容はあくまで zsh 関連の dotfiles の整理に焦点を当てているので、他のソフトウェアによって生成されたファイルについては割愛させていただきます。気になる方は、それぞれのソフトウェアの GitHub レポジトリを調べてみてください。例えば [Oh My Zsh にはキャッシュファイルの生成位置を変えるための環境変数が提供されています](https://github.com/ohmyzsh/ohmyzsh/blob/fd219a94ab585fa699a0e842335a9f33dcbb613b/oh-my-zsh.sh#L103-L106)。

---

最後に、シェルを再起動すれば、zsh 関連の dotfiles の整理が終わります。
他のソフトウェアの dotfiles も同様の手順で対応できると思います。

もし dotfiles の場所変更に対応するための環境変数が提供されていないが、コマンドラインにはオプションで dotfiles の位置を指定できる場合は、下記の例のように alias を作りましょう：

```shell
$ alias command="command --config-file $XDG_CONGIG/command/.config"
```

環境変数もオプションも提供されていない場合は、自分で hack して解決するか、そのソフトウェアだけに対して dotfiles の移動を諦めましょう 💦

# 3. YADM を利用して dotfiles をバージョン管理を行う

Dotfiles の整頓が済んだら、最後はバージョン管理をします。

なぜ dotfiles をバージョン管理するのでしょうか？その理由は大きく 2 つあります：

1. 変更の追跡・比較・復元：過去の設定変更を追跡し、変更の差分を確認したり、誤って行った変更を簡単に元に戻したりすることができる。
2. 複数デバイスの同期：複数のデバイスの間で設定を簡単に同期させることができる。

## YADM

そこで [YADM (Yet Another Dotfiles Manager)](https://yadm.io/) の出番です。

### GNU Stow との比較

ここで疑問を抱く人もいるかもしれません。
dotfiles を管理する手段としてよく使われている [GNU Stow](https://www.gnu.org/software/stow/) があります。では、なぜわざわざ YADM を選ぶのでしょうか？

GNU Stow を始めたいくつかのツールは symlink で dotfiles を管理しています。そのため、まず dotfiles を特定のディレクトリーに移動させる必要があります。それに対して、YADM は dotfiles の配置場所に制約を受けずに管理できます。この記事では XDG 仕様で dotfiles を整頓しているため、この点において YADM は有利です。

### git との比較

では git を使うのはどうでしょうか？

実際、YADM は git のラッパーです。`yadm` のサブコマンドは基本的に `git` と同じように使用します。branch、merge、rebuse、submodule などの機能がすべて使えます。

git を直接に使うよりも、YADM を使った方がいくつかの利点があります。

まず、YADM はデフォルトで `$XDG_CONFIG_HOME/yadm/repo.git` にベアリポジトリ（bare repository）を作って、それをデフォルトのリポジトリとして使います。そのため、現在のディレクトリが git リポジトリであるかどうか、また現在のディレクトリがどこであるかに関わらず、`yadm` の操作を行うことができます。さらに、デフォルトリポジトリがベアリポジトリとなるため、ユーザーが追加したファイルのみが git の追跡対象になります。もし `$HOME` 内で通常の git リポジトリを作ったら、そのリポジトリは再帰的に `$HOME` 内の全てのファイルが追跡対象にすることになります。

### 実際に YADM を試してみよう

実際に YADM を使って、先ほど整頓した zsh の dotfiles をバージョン管理してみましょう：

```shell
$ brew install --formula yadm
$ cd ~
$ yadm init
$ yadm add $XDG_CONFIG_HOME/zsh
$ yadm commit
# GitHub で新しくリポジトリを作成してください。
$ yadm remote add origin [your remote repository]
# Push する前には Git やリモートリポジトリの設定を済ましておいてくださいね。
$ yadm push -u origin main
```

これによって、zsh の dotfiles が YADM でバージョン管理されるようになります。
以降は、いつも `git` を操作するように `yadm` を使えば、ファイルの変更を追跡したり、同期させたりすることが楽にできます。

### YADM の他の便利な機能

実は YADM はバージョン管理の他に、いくつか便利な機能も提供しています。

#### 1. Bootstrap

今 dotfiles は `$ yadm clone` で簡単に同期させることができるものの、多くの場合、環境設定はそれだけじゃ終わりません。例えば、macOS の場合、[Homebrew](https://brew.sh/) や `homebrew` でソフトウェアをインストールする必要があるかもしれません。こうした環境設定はシェルスクリプトを書けば簡単にできますね。

このように、コマンドを自動的に読み込んで実行することは、英語では「Bootstrap」と言います。

YADM は `$ yadm bootstrap` というサブコマンドを提供しています。これはデフォルトで `$HOME/.config/yadm/bootstrap` のシェルスクリプトを実行します。

もし bootstrap ファイルが存在する場合、`$ yadm clone` の操作が完了すると、bootstrap ファイルを実行するかどうかのプロンプトが表示されます。それで楽に環境設定できます。

```
Found .config/yadm/bootstrap
It appears that a bootstrap program exists.
Would you like to execute it now? (y/n)
```

#### 2. 環境固有の dotfile

たまに、異なる OS、アーキテクチャ、ユーザーに対して異なる設定が必要な場合があります。YADM はこのようなケースに対して 2 つの方法を提供しています。

一つは、次のように一つの dotfile に対して複数のバージョンを作って、ファイル名の後ろに条件を付けます。

```
$HOME/path/example.txt##default
$HOME/path/example.txt##os.Darwin
$HOME/path/example.txt##os.Linux,hostname.host1
```

もう一つはテンプレートを使います。これは、dotfile の一部だけが変化した場合には便利です。

```jinja
{% if yadm.os == "Darwin" %}
This block is included for MacOS
{% else %}
This block is included for any other OS
{% endif %}
```

#### 3. 暗号化

SSH キーなどの機密ファイルはもちろんバージョン管理できます。しかし、平文のまま GitHub などの公開システムにプッシュするのはあまりにも危険です。YADM は簡単に機密ファイルを暗号化・復号できます。しかし、この機能は `gpg` か `openssl` コマンドが使える場合でしか使えないです。具体的な方法は [YADM の公式ドキュメント](https://yadm.io/docs/overview) を読んでください。

#### 4. Hook

YADM には、git hook のように、hook を使うことができます。

例えば、通常 git リポジトリには `README` と `LICENSE` などのファイルが含まれています。しかし、そのままクローンすると、それらのファイルが `$HOME` を汚します。そこで、post_clone の hook と sparse-checkout 機能を使えば、クローンが完了すると特定のファイルを削除することができます。

```shell
# ~/.config/yadm/hooks/post_clone

# このスクリプトは https://sspai.com/post/66894 より引用
# remove yadm readme
if [[ ! -f "${YADM_HOOK_REPO}/info/sparse-checkout" ]]; then
  yadm gitconfig core.sparseCheckout true
  cat << EOF > "${YADM_HOOK_REPO}/info/sparse-checkout"
# Generated by $0
/*
!README.md
!UNLICENSE
EOF
  yadm checkout --quiet
fi
```

この例では、`README.md` と `UNLICENSE` ファイルを除いてすべてのファイルをクローンするための設定を行っています。これにより、必要なファイルだけが `$HOME` 内にコピーされるようになります。

---

より詳しい説明は [YADM の公式ドキュメント](https://yadm.io/docs/overview) を読んでください。

## 終わりに

初めて Qiita での投稿、最後まで読んでいただき、ありがとうございました！
自分の開発環境を整え、効率的に管理する方法について共有できて嬉しいです。
YADM についての記事は qiita でもおそらく初めてのものです。
この記事が他の方々の役に立ち、新しい知識やアイデアのひらめきを提供できることを願っています。

最後に私の [dotfiles リポジトリ](https://github.com/LuckyWindsck/dotfiles)を共有します。

## 参考文献

1. [使用 YADM 整理你的 dotfiles](https://sspai.com/post/66894)（中国語簡体字）
2. [XDG Base Directory Specification の仕様](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)（英語）
3. [XDG 仕様 をサポートするソフトウェアのリスト](https://wiki.archlinux.org/title/XDG_Base_Directory)（英語）
4. [上記リストの日本語バージョン](https://wiki.archlinux.jp/index.php/XDG_Base_Directory)（日本語）
5. [xdg-ninja のリポジトリ](https://github.com/b3nj5m1n/xdg-ninja)（英語）
6. [zsh startup files のドキュメント](https://zsh.sourceforge.io/Intro/intro_3.html)（英語）
7. [\[Reddit\] /etc/zshenv vs /etc/zsh/zshenv](https://www.reddit.com/r/zsh/comments/ewzrr6/etczshenv_vs_etczshzshenv/#t1_fg5ri1j)（英語）
8. [YADM の公式サイト](https://yadm.io/)（英語）