どこでも動く dotfiles の作り方

今年も残りわずかになりましたね。年末年始のお休みを利用して、皆さんの設定ファイルを大掃除してみてはいかがですか？

ということで、今回は **dotfiles** のお話です。

私流の dotfiles のサンプルは [https://github.com/Gandats/dotfiles-sample](https://github.com/Gandats/dotfiles-sample) に置いてあります。
この記事を読まなくても、このサンプルの `README.md` を読んで、必要なファイルをコピーすれば便利に管理できるかもしれません。

# dotfiles とは？

**dotfiles** とは、普段自分が使っているソフトウェアの設定ファイルを管理するリポジトリです。Unix系のOSでは設定ファイルは慣例的に `.` から始まる隠しファイル( `.bashrc` や `.vimrc` など）になっていることからこう呼ばれています。

これらをまとめてバージョン管理しておくことで、どこでも一瞬で自分好みの快適な環境を作り出せます！


# ステップ1. dotfilesを始めよう

私流の dotfiles のサンプルは [https://github.com/Gandats/dotfiles-sample](https://github.com/Gandats/dotfiles-sample) に置いてあります。

## まずは最小限の dotfiles

1. GitHub で `dotfiles` というリポジトリを作成
2. 作成したリポジトリをホームディレクトリ直下に clone
3. 各種設定ファイルをリポジトリ内にコピー （例： `mv ~/.bashrc ~/dotfiles/.bashrc` ）
4. もとの場所にシンボリックリンクを作成 （例： `ln -s ~/dotfiles/.bashrc ~/.bashrc` ）
5. コミット & プッシュ
6. 新しい環境では clone してシンボリックリンクを作成

## dotfiles で管理すべきものとそうでないもの

### 管理すべきものの例
- シェルの設定
    - `.bashrc` `.bash_profile` `.zshrc` `.zshenv`
- エディタの設定
    - `.vimrc` `.emacs.d/init.el`
- CLIツールの設定
    - `.tmux.conf` `.tigrc`

### 管理すべきでないものの例（ `.gitignore` に追加する）
- 常に自動で更新されるファイル（履歴やキャッシュ等）
    - `.bash_history` `.zsh_history` `.zcompcache` `.zcompdump` `.viminfo` `.emacs.d/history` 等
- バックアップファイル
    - `*~` `*.swp` 等
- 環境に大きく依存する設定（参考: [Tips](#tips)）
    - 環境変数 `PATH` や `LD_LIBRARY_PATH` 等に明らかに環境固有の場所を追加するとき
    - CUDAの設定

### 管理してはいけないものの例
- 認証情報が入ったディレクトリやファイル
    - `.ssh` `.aws`


# ステップ2. 自動でシンボリックリンクを作成するスクリプトを書こう

私流の dotfiles のサンプルは [https://github.com/Gandats/dotfiles-sample](https://github.com/Gandats/dotfiles-sample) に置いてあります。

毎回、新しい環境でシンボリックリンクを作成し直すのは（特に設定ファイルが増えると）意外と大変です。
自動でシンボリックリンクを作成するスクリプトを書いてみましょう。

## 自動スクリプトを作成する
1. リポジトリ直下に `dotfiles` というディレクトリを作って、設定ファイルをそこに移動してください。

    ```sh:ターミナルにて
    cd <リポジトリのパス>
    mkdir dotfiles
    mv .* dotfiles
    ```

2. リポジトリ直下に `scripts` ディレクトリを作成し、そこに `link.sh` を作成。

    ```sh:ターミナルにて
    mkdir scripts
    cd scripts
    touch link.sh
    ```

3. `link.sh` を編集

    ```sh:link.sh
    #!/bin/sh

    dotfiles_root=$(cd $(dirname $0)/.. && pwd)

    # dotfilesディレクトリの中身のリンクをホームディレクトリ直下に作成
    cd ${dotfiles_root}/dotfiles
    for file in .*; do
        ln -s ${PWD}/${file} ${HOME}
    done
    ```

4. 実行権限を付与

    ```sh:ターミナルにて
    chmod +x link.sh
    ```

これでターミナルで `./link.sh` を実行するとシンボリックリンクが自動的に作成されるようになります。

このときのリポジトリのディレクトリ構造の例です。

```
├── .gitignore
├── dotfiles  # 設定をまとめるディレクトリ
│   ├── .bashrc
│   ├── .vim
│   │   └── ...
│   └── .zshrc
└── scripts         # リンク作成等のスクリプトをまとめるディレクトリ
    └── link.sh     # リンク作成スクリプト
```


# ステップ3. どこでも動く dotfiles にしてみよう

私流の dotfiles のサンプルは [https://github.com/Gandats/dotfiles-sample](https://github.com/Gandats/dotfiles-sample) に置いてあります。

## 展開場所を考える

ステップ2で作成したスクリプトにはまだいくつかの問題点があります。

**ケース1**
neovim のように設定ファイルがホームディレクトリ直下でないソフトウェアに対応できない
- neovim の設定ファイルは `~/.config/nvim/init.vim`

**ケース2**
VSCode のようにOSごとに設定ファイルの置き場所が異なるソフトウェアに対応できない
- Linux では `~/.config/Code/User/settings.json`
- macOS では `~/Library/ApplicationSupport/Code/User/settings.json`

### ケース1 任意の場所にシンボリックリンクを作成

ここでは、 `dotfiles` に `linklist.txt` を作成して、そこに展開場所を記述しましょう。

#### `linklist.txt` の書き方
- 各行は空白で区切られた2つのフィールド
    - 1つめはターゲット（ `dotfiles` 内のパス）
    - 2つめはリンクのパス
        - `~` や環境変数が利用可能
- `#` 以降や空行は無視される
- 不要な設定は `script/link.sh` の実行前に `#` でコメントアウトすることでリンクが作成されない

```sh:linklist.txt
# vim
.vim                ${HOME}/.vim

# neovim
nvim                ${HOME}/.config/nvim

# zsh
.zsh.d              ${HOME}/.zsh.d
.zshenv             ${HOME}/.zshenv
```

#### `link.sh` を修正
`linklist.txt` の内容を読むように `link.sh` の内容を修正しましょう。

```sh:link.sh
#!/bin/sh

dotfiles_root=$(cd $(dirname $0)/.. && pwd)

# linklist.txtのコメントを削除
__remove_linklist_comment() {(
    # '#'以降と空行を削除
    sed -e 's/\s*#.*//' \
        -e '/^\s*$/d' \
        $1
)}

# シンボリックリンクを作成
cd ${dotfiles_root}/dotfiles
linklist="linklist.txt"
[ ! -r "$linklist" ] && return
__remove_linklist_comment "$linklist" | while read target link; do
    # ~ や環境変数を展開
    target=$(eval echo "${PWD}/${target}")
    link=$(eval echo "${link}")
    # シンボリックリンクを作成
    mkdir -p $(dirname ${link})
    ln -fsn ${target} ${link}
done
```

### ケース2 OSごとにシンボリックリンクを切り替える

Unix系OSでは `uname` コマンドによってOSの名前（ `Linux` `Darwin` `FreeBSD` 等）を取得できます。
ここでは、この値を利用してシンボリックリンクを切り替えてみましょう。
（より詳細に切り替えたい方は `OSTYPE` 環境変数などを利用しても良いかもしれません。）

#### `linklist.txt` をOSごとに作成
先程まで利用していた `linklist.txt` を以下のように変更します。(FreeBSD等に対応させる場合はLinuxやDarwinの部分を `uname` に置き換えてください。)

- `linklist.Unix.txt`
    - Unix系OSに共通のもの依存しないもの（ `~/.bashrc` など） 
- `linklist.Linux.txt`
    - Linuxに固有のもの（ `~/.config/Code/User/settings.json` など）
- `linklist.Darwin.txt`
    - macOSに固有のもの（ `~/Library/ApplicationSupport/Code/User/settings.json` など）

```sh:linklist.Unix.txt
# vim
.vim                ${HOME}/.vim

# neovim
nvim                ${HOME}/.config/nvim

# zsh
.zsh.d              ${HOME}/.zsh.d
.zshenv             ${HOME}/.zshenv
```

```sh:linklist.Linux.txt
# Visual Studio Code
Code/User/settings.json      ${HOME}/.config/Code/User/settings.json
Code/User/keybindings.json   ${HOME}/.config/Code/User/keybindings.json
Code/User/locale.json        ${HOME}/.config/Code/User/locale.json
Code/User/snippets           ${HOME}/.config/Code/User/snippets
```

```sh:linklist.Darwin.txt

# Visual Studio Code
Code/User/settings.json      ${HOME}/Library/ApplicationSupport/Code/User/settings.json
Code/User/keybindings.json   ${HOME}/Library/ApplicationSupport/Code/User/keybindings.json
Code/User/locale.json        ${HOME}/Library/ApplicationSupport/Code/User/locale.json
Code/User/snippets           ${HOME}/Library/ApplicationSupport/Code/User/snippets
```

#### `link.sh` を修正
これに対応して `link.sh` も修正します。

```sh:link.sh
#!/bin/sh

# ~~~ 省略 ~~~

cd ${dotfiles_root}/dotfiles
for linklist in "linklist.Unix.txt" "linklist.$(uname).txt"; do
    [ ! -r "${linklist}" ] && continue

    __remove_linklist_comment "$linklist" | while read target link; do
        # ~ や環境変数を展開
        target=$(eval echo "${PWD}/${target}")
        link=$(eval echo "${link}")
        # シンボリックリンクを作成
        mkdir -p $(dirname ${link})
        ln -fsn ${target} ${link}
    done
done
```

# Tips
- gitのコミットメッセージの先頭にソフトウェア名を入れると見やすくなり、revert時などに便利です。
    - 例： *[vim] 行番号を表示するように変更* 、 *[bash] 履歴を10,000件保存するように変更*
- `link.sh` 同様に `unlink.sh` もあると意外と便利です。
- シェルの設定は機能ごとにファイルを分割すると管理しやすいです。
    - 環境固有の設定ファイルを `example.local.sh` とし、 `.gitignore` に `*.local.sh` を追加すると自動的に管理対象外となります。
- 本当にどこでも動くようにするにはシェルスクリプトをPOSIXコマンド・オプションのみで書くと良いかもしれません
    - 今回の例では `ln` コマンドの `-n` オプションはPOSIXの範囲外です。

# 最後に
dotfiles の作り方をいくつかのステップに分けたため、予想以上に長くなってしまいました。

私のサンプルが [https://github.com/Gandats/dotfiles-sample](https://github.com/Gandats/dotfiles-sample) にあるので、よかったら参考にしてみてください。