会社でもプライベートでもdotfilesを管理したい

# はじめに
**<font color="OrangeRed">※ 本管理方法のご使用は、自己責任でお願いします。</font>**
会社用/研究用PCでもプライベートPCでも同じdotfiles環境を使用したいケースはあると思います。
ただ、会社内の情報など、会社用PCのdotfilesには記載したいけど、プライベートPCには記載したくないなど、単純に同じdotfilesを使用することはできません。

本記事では、会社用PCでもプライベートPCでもdotfiles環境を棲み分けつつ管理する方法を紹介します。

私のdotfilesは[narugit/dotfiles](https://github.com/narugit/dotfiles)にて管理しています。

# 想定読者
- 同じdotfiles環境を複数PCで使用したい人
- dotfiles環境の管理方法に悩んでいる人

# 筆者の環境
私は、下記環境でdotfilesを運用しています。

- PC1
    - OS: Ubuntu 20.04
    - 管理しているdotfiles
        - `.zshrc`
        - `.tmux.conf`
        - `.gitconfig`
        - `.ssh/config`
        - `.vimrc`

- PC2
    - OS: macOS Big Sur (Intel CPU)
    - 管理しているdotfiles
        - `.zshrc`
        - `.tmux.conf`
        - `.gitconfig`
        - `.ssh/config`
        - `.vimrc`

本文のコード例では筆者の環境をベースに説明しますが、管理方法の考え方自体はファイル分割不可能なdotfileでも無い限り、他のdotfileでも応用可能だと思います。
また、私はzshユーザーであるため説明内では`.zshrc`を持ち出して説明しますが、本管理をするにあたりzsh onlyな機能を使用していないため、`.bashrc`に読み替えて頂いても問題ございません。

# 目次

[1章](#1章-gitで管理するには)は全員読んでください。
GitHubに上げられない秘匿情報がある方は[3章](#3章-シークレットな情報はgithubにあげないようにする)を、会社PCと自宅PCでdotfilesを共通化したい方は[4章](#4章-プライベートアカウントでcommitできるようにする)を読んでください。
より効率化したい方は、[2章](#2章-dotfilesの分割)、[5章](#5章-リモートとローカルのdotfiles差分に気づけるようにする)、[6章](#6章-ワンライナーで環境構築)を読んでください。

|章|タイトル|
|----|----|
|1章|[Gitで管理するには](#1章-gitで管理するには)|
|2章|[dotfilesの分割](#2章-dotfilesの分割)|
|3章|[シークレットな情報はGitHubにあげないようにする](#3章-シークレットな情報はgithubにあげないようにする)|
|4章|[プライベートアカウントでcommitできるようにする](#4章-プライベートアカウントでcommitできるようにする)|
|5章|[リモートとローカルのdotfiles差分に気づけるようにする](#5章-リモートとローカルのdotfiles差分に気づけるようにする)|
|6章|[ワンライナーで環境構築](#6章-ワンライナーで環境構築)|

# 1章 Gitで管理するには
<details><summary>本文</summary><div>

## 1.1 ユースケース
育て上げたdotfilesを今後他のPCにも移植できるようクラウド上にアップロードしておきたいとき。

## 1.2 解決したい課題
ローカル環境にしかないdotfilesをクラウド上で管理したい。

## 1.3 アプローチ
dotfilesをGitで管理できるようにし、GitHub上にアップロードする。

## 1.4 実現方法
ここでは、dotfilesとして`.zshrc`を例に説明する。
最終的には下記のようなディレクトリ構造となる。

```console
${HOME}
    |- .zshrc -> ${HOME}/dotfiles/etc/zsh/.zshrc
    `- dotfiles/
        |- .git/
        `- etc/
            `- zsh
                `- .zshrc
```

1. dotfilesのバックアップを取っておく。

    ```console
    $ mkdir ~/dotfiles.bk
    $ cp ~/.zshrc ~/dotfiles.bk/
    ```

1. dotfiles配置用のディレクトリを作成する。

    ```console
    $ DOTFILES_DIR="${HOME}/dotfiles"
    $ ZSHRC_DIR="${DOTFILES_DIR}/etc/zsh"
    $ mkdir -p "${ZSHRC_DIR}"
    ```

1. dotfiles配置用ディレクトリをGitで管理する。

    ```console
    $ cd "${DOTFILES_DIR}"
    $ git init
    $ git commit --allow-empty -m "initial commit"
    ```

1. dotfiles配置用ディレクトリにdotfilesを移動する。

    ```console
    $ mv ~/.zshrc "${ZSHRC_DIR}"
    ```

1. dotfilesのシンボリックリンクを作成する。

    ```console
    $ ln -snfv "${ZSHRC_DIR}"/.zshrc ~/.zshrc
    ```

1. ターミナルを再起動し、dotfilesが問題無く読み込まれているか確認する。

以上の手順で、dotfilesをGitで管理することができるようになった。
適宜、GitHub上にpushすることでローカルのdotfilesをGitHub上にアップロードすることができる。

## 1.5 メリット
- dotfilesをバージョン管理できるようになる。
- PC乗り換えの際に、`git clone`して、シンボリックリンクを作成するだけで同じdotfiles環境を作り上げることができる。

## 参考リンク
- [dotfilesをGitHubで管理](https://qiita.com/okamos/items/7f5461814e8ed8916870)

</div></details>

# 2章 dotfilesの分割
<details><summary>本文</summary><div>

## 2.1 ユースケース
便利な機能を見つけ、dotfilesを修正したい。
ファイルのどの行に新機能に向けた行を追加しても動作はするが、似た機能同士は近くの行に配置したいとき。

## 2.2 解決したい課題
新機能向けの行を追加すべき箇所がどこなのか把握しやすくしたい。

## 2.3 アプローチ
機能のジャンルごとにファイルを分割することで、新機能を追加する際にジャンルからどのファイルを修正すべきか把握できるようにする。

## 2.4 実現方法
ここより先は、dotfilesごとにどう分割するかの具体例を記載する。 利用するdotfilesの項目だけご確認ください。

基本的には、エントリーポイントとなるファイルが分割したファイルを読み込むような構成となっている。 ファイルの分割ルール（機能ごとに分ける、ジャンルごとに分けるなど）や分割後のファイル名などは適宜自分好みに設定してください。

### zsh
ディレクトリ構成は下記の通り。

```console
${HOME}/
    |- .zshrc -> ${HOME}/dotfiles/etc/zsh/.zshrc
    |- .zsh.d -> ${HOME}/dotfiles/etc/zsh/.zsh.d
    `- dotfiles/
        `- etc/
            `- zsh/
                |- .zshrc
                `- .zsh.d/
                    |- alias.zsh
                    |- audit.zsh
                    |- completion.zsh
                    |- peco.zsh
                    |- plugin.zsh
                    |- powerlevel10k.zsh
                    `- priorities.conf
```

下記のように各ファイル/ディレクトリに役割を与えている。

- `.zshrc`: エントリーポイント。`.zsh.d`配下のファイルを読み込む。
- `.zsh.d/*.zsh`: もともとの`.zshrc`を分割したファイル群
- `.zsh.d/priorities.conf`: `.zsh.d/*.zsh`の各ファイルををどの順で読み込むかを記載している。
- `.zsh.d/audit.zsh`: `priorities.conf`に記載されていないのに`.zsh.d/`配下の`*.zsh`ファイルがある場合に標準出力にメッセージを表示する。

各ファイルの中身は下記の通り。

```zsh:.zshrc
ZSH_CONFS_DIR="${HOME}/.zsh.d"
ZSH_PRIORITIES_CONF="${ZSH_CONFS_DIR}/priorities.conf"

# Load ${ZSH_CONFS} in ${ZSH_PRIORITIES_CONF}
source "${ZSH_PRIORITIES_CONF}"

for zsh_conf in ${ZSH_CONFS}; do
  source "${ZSH_CONFS_DIR}/${zsh_conf}"
done
```

```zsh:priorities.conf
# Be careful about the order of settings, as they are loaded in order from the top.
ZSH_CONFS=(
"audit.zsh" \
"plugin.zsh" \
"powerlevel10k.zsh" \
"peco.zsh" \
"completion.zsh" \
"alias.zsh" \
)
```

```zsh:audit.zsh
DOTFILES_DIR="${HOME}/dotfiles"

zsh_d_list=($(find "${HOME}/.zsh.d/"*.zsh -type f | awk -F '/' '{ print $NF }' | sort))
source "${HOME}/.zsh.d/priorities.conf"
zsh_confs=$(printf "%s\n" "${ZSH_CONFS[@]}" | sort)

invaders=$(echo ${zsh_d_list[@]} ${zsh_confs[@]} | tr ' ' '\n' | sort | uniq -u | tr '\n' ' ')

if test -n "${invaders}"; then
  echo "Not managed in ${HOME}/.zsh.d/priorities.conf: ${invaders}"
fi
```

### tmux
ディレクトリ構成は下記の通り。

```console
${HOME}/
    |- .tmux.conf -> ${HOME}/dotfiles/etc/tmux/.tmux.conf
    |- .tmux.d/ -> ${HOME}/dotfiles/etc/tmux/.tmux.d
    `- dotfiles/
        `- etc/
            `- tmux/
                |- .tmux.conf
                `- .tmux.d/
                    |- continuum.tmux
                    |- keybinding.tmux
                    |- pane.tmux
                    `- stateline.tmux
```

```plaintext:.tmux.conf
TMUX_CONFS_DIR="${HOME}/.tmux.d"
source-file ${TMUX_CONFS_DIR}/keybinding.tmux
source-file ${TMUX_CONFS_DIR}/stateline.tmux
source-file ${TMUX_CONFS_DIR}/pane.tmux
source-file ${TMUX_CONFS_DIR}/continuum.tmux

set -g @plugin 'tmux-plugins/tpm'

# Plugins
set -g @plugin "tmux-plugins/tmux-battery"
set -g @plugin "tmux-plugins/tmux-continuum"
set -g @plugin "tmux-plugins/tmux-cpu"
set -g @plugin "tmux-plugins/tmux-prefix-highlight"
set -g @plugin "tmux-plugins/tmux-resurrect"
set -g @plugin "tmux-plugins/tmux-sensible"
set -g @plugin "narugit/tmux-storage-status"
set -g @plugin "narugit/tmux-temp"
set -g @plugin "samoshkin/tmux-plugin-sysstat"

# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run -b '~/.tmux/plugins/tpm/tpm'
```

### ssh
ディレクトリ構成は下記の通り。

```console
${HOME}/
    |- .ssh
        |- config -> ${HOME}/dotfiles/etc/ssh/config
        `- .config.d/ -> ${HOME}/dotfiles/etc/ssh/.config.d/[darwin|linux]
    `- dotfiles/
        `- etc/
            `- ssh/
                |- config
                `- .config.d/
                    |- darwin/
                    `- linux/
```

```plaintext:config
Include .config.d/*
```

### vim
ディレクトリ構成は下記の通り。

```console
${HOME}/
    |- .vimrc -> ${HOME}/dotfiles/etc/vim/.vimrc
    |- .vim/
        |- dein/
        |- userconfig/
            |- plugins.toml -> ${HOME}/dotfiles/etc/vim/dein/plugins.toml
            |- plugins_lazy.toml -> ${HOME}/dotfiles/etc/vim/dein/plugins_lazy.toml
        |- init/ -> ${HOME}/dotfiles/etc/vim/init
        `- plugin/ -> ${HOME}/dotfiles/etc/vim/plugin
    `- dotfiles/
        `- etc/
            `- vim/
                |- .vimrc
                |- dein
                    |- plugins.toml
                    `- plugins_lazy.toml
                |- init
                    |- input.vim
                    |- plugin.vim
                    `- view.vim
                `- plugin
                    |- ale.vim
                    `- ctrlp.vim
```

```viml:.vimrc
runtime! init/*.vim
runtime! plugin/*.vim
```

### git
ディレクトリ構成は下記の通り。

```console
${HOME}/
    |- .gitconfig -> dotfiles/etc/git/.gitconfig
    |- .git.d -> dotfiles/etc/git/.git.d
    `- dotfiles/
        `- etc/
            `- git/
                |- .gitconfig
                `- .git.d
                    |- alias.git
                    |- core.git
                    |- init.git
                    |- pager.git
                    `- user.git
```


```ini:.gitconfig
[include]
  path = ~/.git.d/alias.git
  path = ~/.git.d/core.git
  path = ~/.git.d/init.git
  path = ~/.git.d/pager.git
  path = ~/.git.d/user.git
```


## 2.5 メリット
- ファイルを分割したため、各ファイルの行数が少なくなり、ファイルさえ特定できれば修正箇所を追加する場所が分かりやすい。
- ある機能について記載された箇所がどこなのか探しやすい。

## 参考リンク
- [zsh設定ファイル（.zshrc）を分割する](https://fnwiya.hatenablog.com/entry/2015/11/03/191902)
- [vimrc分割した](https://wakame.hatenablog.jp/entry/2014/09/05/085345)

</div></details>

# 3章 シークレットな情報はGitHubにあげないようにする
<details><summary>本文</summary><div>

## 3.1 ユースケース
会社用PCで使うGitHubアカウントの情報やproxy用環境変数などシークレットな環境変数を`.gitconfig`や`.zshrc`、`.ssh/config`に記載したいとき。

## 3.2 解決したい課題
シークレットな情報はGitHubにアップロードせずに、`.gitconfig`, `.zshrc`, `.ssh/config`をGitで運用したい。

## 3.3 アプローチ
gitignoreの仕組みを使って、Git管理外のファイルにシークレットな情報を書き込む。Gitで管理しているファイルからそれらシークレットな情報が記載されたファイルを読み込む。

## 3.4 実現方法
`.zshrc`, `.gitconfig`, `.ssh/config`のそれぞれでどう実現するか説明する。

### zsh
`${HOME}/.zshrc.secret`を作成し、GitHub上にアップロードしたくない、シークレットな情報を記載する。
`.zshrc.secret`を`.gitignore`に記載した上で、`.zshrc`から`.zshrc.secret`を読み込む。
`.gitignore`, `.zshrc`に記載する内容は下記。

```plaintext:.gitignore
.zshrc.secret
```

```shell:.zshrc
ZSH_SECRET_CONF="${HOME}/.zshrc.secret"

if [ -e "${ZSH_SECRET_CONF}" ]; then
  source "${ZSH_SECRET_CONF}"
fi
```

### git
`${HOME}/.gitconfig.secret`を作成し、GitHub上にアップロードしたくない、シークレットな情報を記載する。
`.gitconfig.secret`を`.gitignore`に記載した上で、`.gitconfig`から`.gitconfig.secret`を読み込む。
`.gitignore`, `.gitconfig`に記載する内容は下記。
`.gitconfig`は後勝ち方式なので、`.gitconfig.secret`の情報を最優先させたい場合は、一番最後にincludeされるように記載する。
例えば、`.gitconfig`にプライベートアカウントの情報を記載しており、会社用のPCを使用する場合は、デフォルトのGitHubアカウントは会社用のアカウントにしたい。
この場合は、`.gitconfig.secret`に会社用のアカウント情報を記載し、`.gitconfig`内で最後にincludeして、`.gitconfig.secret`の情報が最優先されるように設定する。

```plaintext:.gitignore
.gitconfig.secret
```

```ini:.gitconfig
[include]
  path = ~/.gitconfig.secret
```

### ssh
`${HOME}/.ssh/config.secret`を作成し、GitHub上にアップロードしたくない、シークレットな情報を記載する。
`config.secret`を`.gitignore`に記載した上で、`.ssh/config`から`.ssh/config.secret`を読み込む。
`.gitignore`, `.ssh/config`に記載する内容は下記。

```plaintext:.gitignore
config.secret
```

```ini:config
Include config.secret
```

## 3.5 メリット
GitHub上にあげても良い情報とそうではない情報を別々で管理することができる。
</div></details>

# 4章 プライベートアカウントでcommitできるようにする
<details><summary>本文</summary><div>

## 4.1 ユースケース
社内PCのデフォルトcommitユーザーが社用アカウントである場合に、dotfilesリポジトリ上でcommitするとき。

## 4.2 解決したい課題
デフォルトcommitユーザーに依らずに、dotfilesリポジトリ上のcommit及びpushを行うユーザーはプライベートアカウントとなるようにしたい。

## 4.3 アプローチ
dotfilesリポジトリの`.git/config`を下記のように修正する。

- push時に使用する秘密鍵がプライベートアカウントに紐付いたものとなるようにする。
- commit時のアカウント情報をプライベートアカウントのものと同一にする。

## 4.4 実現方法
1. プライベートアカウント向けのssh設定を記載する。`.ssh/config`から`.ssh/.config.d/github.config`をIncludeしている前提。

    ```ini:.ssh/.config.d/github.config
    Host github-personal
      User git
      HostName github.com
      IdentityFile ~/.ssh/id_rsa_personal
      Port 22
      TCPKeepAlive yes
      IdentitiesOnly yes
      AddKeysToAgent yes
    ```

1. dotfiles管理リポジトリにて、push及びcommit時にプライベートアカウントを使用するよう設定する。各変数は自身の環境に応じて適宜変更する。`GIT_CONFIG_LOCAL_USER_NAME`, `GIT_CONFIG_LOCAL_USER_EMAIL`はそれぞれ自身のプライベートGitHubアカウントのユーザー名、メールアドレスを設定する。

    ```console
    $ DOTFILES_DIR="${HOME}/dotfiles"
    $ DOTFILES_REPO="narugit/dotfiles.git"
    $ GITHUB_PERSONAL_HOST="github-personal"
    $ cd "${DOTFILES_DIR}"
    $ git remote set-url --push origin "${GITHUB_PERSONAL_HOST}:${DOTFILES_REPO}"
    $ git config --local user.name "${GIT_CONFIG_LOCAL_USER_NAME}"
    $ git config --local user.email "${GIT_CONFIG_LOCAL_USER_EMAIL}"
    ```

1. 実際にダミーcommitを作成し、正しく設定できているか確認する。`git log`でcommit　authorがプライベートアカウントになっているか、またpush dry-run(`-n`)でエラーが出ていなければOK。

    ```console
    $ git switch -c dummy_branch
    $ touch hoge
    $ git add hoge
    $ git commit -m "dummy commit"
    $ git log
    +++ 確認ポイント1: commit authorがプライベートアカウントであることを確認する +++
    $ git push -n origin HEAD
    +++ 確認ポイント2: エラーが出ていないことを確認する +++
    $ git switch -
    $ git branch -D dummy_branch
    ```

## 4.5 メリット
社用アカウントなど、プライベートアカウント以外で社外リポジトリである、dotfiles管理リポジトリに変更修正を加えることができるようになる。
</div></details>

# 5章 リモートとローカルのdotfiles差分に気づけるようにする
<details><summary>本文</summary><div>

## 5.1 ユースケース
PCを複数台使用する場合に、あるPC上のdotfilesのみが更新され、他のPC上のdotfilesの更新を忘れてしまうとき。

## 5.2 解決したい課題
あるPC上でだけdotfilesが更新されている状態をできるだけ短くしたい。

## 5.3 アプローチ
ローカル上のdotfiles管理リポジトリとGitHub上のdotfiles管理リポジトリ間に更新差分がある場合、shellの起動時に標準出力で通知する。

## 5.4 実現方法
大まかな流れとしては、下記の通りである。

1. 10分に1度、GitHub上のdotfiles管理リポジトリをfetchする。

1. shellを起動時に下記いずれかが真の場合、標準出力上にメッセージを表示する。
  - ローカル上にある先頭commit IDとfetchしたリモート相当の先頭commit IDが異なる
  - ローカルリポジトリ上に新規ファイルが存在する


ただ、shellを起動する度に標準出力上にメッセージするのは鬱陶しいため、ガッツリ開発中な時間帯はdotfilesの管理に時間を割けないと考え、指定した時間帯にはメッセージの出力頻度を抑えるようにしている。

また、定期的にfetchする方法がMacとUbuntuとで異なるため、それぞれ分けて説明する。

最終的なディレクトリ構成は下記の通り。

### Mac

```console
/usr/local/bin/
    `- dotfiles_fetch.sh -> ${HOME}/dotfiles/bin/dotfiles_fetch.sh

${HOME}/
    |- .zshrc -> ${HOME}/dotfiles/etc/zsh/.zshrc
    |- .zsh.d/ -> ${HOME}/dotfiles/etc/zsh/.zsh.d
    |- Library/
        `- LaunchAgents/
            `- dotfiles/ -> ${HOME}/dotfiles/etc/launchd
    `- dotfiles/
        |- bin/
            |- dotfiles_compare.sh
            `- dotfiles_fetch.sh
        `- etc/
            |- launchd/
                `- dotfiles-fetch.plist
            `- zsh/
                |- .zshrc
                `- .zsh.d/
                    `- dotfiles_monitor.zsh
```

### Ubuntu

```console
/usr/local/bin/
    `- dotfiles_fetch.sh -> ${HOME}/dotfiles/bin/dotfiles_fetch.sh

${HOME}/
    |- .config/
        `- systemd/
            `- user/
                |- dotfiles-fetch.service -> ${HOME}/dotfiles/etc/systemd/dotfiles-fetch.service
                `- dotfiles-fetch.timer -> ${HOME}/dotfiles/etc/systemd/dotfiles-fetch.timer
    |- .zshrc -> ${HOME}/dotfiles/etc/zsh/.zshrc
    |- .zsh.d/ -> ${HOME}/dotfiles/etc/zsh/.zsh.d
    `- dotfiles/
        |- bin/
            |- dotfiles_compare.sh
            `- dotfiles_fetch.sh
        `- etc/
            |- systemd/
                |- dotfiles-fetch.service
                `- dotfiles-fetch.timer
            `- zsh/
                |- .zshrc
                `- .zsh.d/
                    `- dotfiles_monitor.zsh
```

各ファイルの役割は下記の通り。

- `dotfiles-fetch.plist` (**Mac**): launchdの機能を使用して、10分に1度、`dotfiles_fetch.sh`を実行する。
- `dotfiles-fetch.service` (**Ubuntu**): `dotfiles_fetch.sh`を実行する。
- `dotfiles-fetch.timer` (**Ubuntu**): systemdの機能を使用して、10分に1度、`dotfiles-fetch.service`を起動する。
- `dotfiles_fetch.sh`: GitHub上のdotfiles管理リポジトリからローカル管理リポジトリにfetchする。
- `dotfiles_monitor.zsh`: ガッツリ開発中な時間帯はたまに`dotfiles_compare.sh`を実行する。
- `dotfiles_compare.sh`: 下記がいずれかが真な場合は、標準出力上にメッセージ を出力する。
  - ローカル上にある先頭commit IDとfetchしたリモート相当の先頭commit IDが異 なる
  - ローカルリポジトリ上に新規ファイルが存在する

環境構築手順を紹介する。

1. (**Macの場合**) `dotfiles-fetch.plist`を作成する。

    ```console
    $ DOTFILES_DIR="${HOME}/dotfiles"
    $ PLIST_LOG_DIR="/var/log/dotfiles"
    $ sudo mkdir -p "${PLIST_LOG_DIR}"
    $ sudo chmod 777 "${PLIST_LOG_DIR}"
    $ editor ${DOTFILES_DIR}/etc/launchd/dotfiles-fetch.plist
    ```

    ```xml:dotfiles-fetch.plist
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
        <dict>
            <key>Label</key>
            <string>dotfiles.fetch.plist</string>
            <key>ProgramArguments</key>
            <array>
                <string>/usr/local/bin/dotfiles_fetch.sh</string>
            </array>
            <key>StartCalendarInterval</key>
            <array>
                <dict>
                    <key>Minute</key>
                    <integer>0</integer>
                </dict>
                <dict>
                    <key>Minute</key>
                    <integer>10</integer>
                </dict>
                <dict>
                    <key>Minute</key>
                    <integer>20</integer>
                </dict>
                <dict>
                    <key>Minute</key>
                    <integer>30</integer>
                </dict>
                <dict>
                    <key>Minute</key>
                    <integer>40</integer>
                </dict>
                <dict>
                    <key>Minute</key>
                    <integer>50</integer>
                </dict>
            </array>
            <key>StandardOutPath</key>
            <string>/var/log/dotfiles/fetch.out</string>
            <key>StandardErrorPath</key>
            <string>/var/log/dotfiles/fetch.err</string>
        </dict>
    </plist>
    ```

1. (**Ubuntuの場合**) `dotfiles-fetch.service`, `dotfiles-fetch.timer`を作成する。

    ```console
    $ DOTFILES_DIR="${HOME}/dotfiles"
    $ editor ${DOTFILES_DIR}/etc/systemd/dotfiles-fetch.service
    $ editor ${DOTFILES_DIR}/etc/systemd/dotfiles-fetch.timer
    ```

    ```ini:dotfiles-fetch.service
    [Unit]
    Description=Fetch master from narugit/dotfiles
    
    [Service]
    ExecStart=/usr/local/bin/dotfiles_fetch.sh
    Type=oneshot
    
    [Install]
    WantedBy=timers.target
    ```

    ```ini:dotfiles-fetch.timer
    [Unit]
    Description=exec every 10 minutes
    
    [Timer]
    OnCalendar=*-*-* *:0/10:00
    
    [Install]
    WantedBy=timers.target
    ```

1. `dotfiles_fetch.sh`を作成する。

    ```console
    $ editor ${DOTFILES_DIR}/bin/dotfiles_fetch.sh
    ```

    ```shell:dotfiles_fetch.sh
    #!/bin/bash
    set -o pipefail
    DOTFILES_DIR="${HOME}/dotfiles"

    log_print() {
      if [ -p /dev/stdin ]; then
        local arg=$(cat /dev/stdin)
      else
        local arg="$@"
      fi
      local file_name=${BASH_SOURCE[1]##*/}
      if [ -n "${arg}" ]; then
        echo -e "$(date '+%Y-%m-%d %H:%M:%S') ${file_name} (${file_name}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $arg"
      fi
    }
    
    log_error() {
      if [ -p /dev/stdin ]; then
        local arg=$(cat /dev/stdin)
      else
        local arg="$@"
      fi
      local file_name=${BASH_SOURCE[1]##*/}
      if [ -n "${arg}" ]; then
            >&2 echo -e "$(date '+%Y-%m-%d %H:%M:%S') ${file_name} (${file_name}:${BASH_LINENO[0]}:${FUNCNAME[1]}) ERROR: $arg"
      fi
    }
    
    fetch_command() {
      (cd "${DOTFILES_DIR}" && git fetch -q origin master)
    }
    
    fetch() {
      (fetch_command | log_print) 2>&1 | log_error
      local ret="$?"
      if [ "$ret" = 0 ]; then
        log_print "Success to fetch."
      else
        log_error "Failed to fetch."
      fi
    }
    
    fetch
    ```

1. `dotfiles_compare.sh`を作成する。`DOTFILES_REMOTE_URL`は適宜変更する。

    ```console
    $ editor ${DOTFILES_DIR}/bin/dotfiles_comapre.sh
    ```

    ```shell:dotfiles_comapre.sh
    #!/bin/bash
    DOTFILES_DIR="${HOME}/dotfiles"
    DOTFILES_REMOTE_URL="https://github.com/narugit/dotfiles"

    is_hash_same() {
      local DOTFILES_DEFAULT_BRANCH="master"
      local DOTFILES_REMOTE_HASH=$(cd ${DOTFILES_DIR} && git rev-parse origin/${DOTFILES_DEFAULT_BRANCH})
      local DOTFILES_LOCAL_HASH=$(cd ${DOTFILES_DIR} && git rev-parse ${DOTFILES_DEFAULT_BRANCH})
    
      if [ -z "${DOTFILES_REMOTE_HASH}" ]; then
        echo "Please set ${HOME}/.ssh/id_rsa_personal"
        false
      else
        if [ "${DOTFILES_LOCAL_HASH}" = "${DOTFILES_REMOTE_HASH}" ]; then
          true
        else
          false
        fi
      fi
    }
    
    is_local_clean() {
      if [ -z "$(cd ${DOTFILES_DIR} && git status --porcelain)" ]; then
        true
      else
        false
      fi
    }
    
    echo "Comparing remote dotfiles"
    if is_hash_same && is_local_clean; then
      echo "Your local dotfiles may be up to date at least in a hour!"
    else
      echo "Your local dotfiles differs from remote dotfiles. Please check ${DOTFILES_DIR} and ${DOTFILES_REMOTE_URL}"
    fi
    ```

1. `dotfiles_monitor.zsh`を作成する。`ACTIVE_HOUR_START`から`ACTIVE_HOUR_END`の時間帯は、1/`RARITY_ACTIVE_HOUR`の確率で`dotfiles_compare.sh`を実行する。

    ```console
    $ editor ${DOTFILES_DIR}/etc/zsh/.zsh.d/dotfiles_monitor.zsh
    ```

    ```shell:dotfiles_monitor.zsh
    DOTFILES_DIR="${HOME}/dotfiles"

    ACTIVE_HOUR_START=10
    ACTIVE_HOUR_END=17
    
    # rarity: one time in how many times to compare with remote's dotfiles.
    RARITY_MIN=1
    RARITY_ACTIVE_HOUR=10
    RARITY_NON_ACTIVE_HOUR=1
    
    generate_random() {
      local min="$1"
      local max="$2"
      local random_num=$(awk -v min="$min" -v max="$max" 'BEGIN{srand(); print int(min+rand()*(max-min+1))}')
      echo "$random_num"
    }
    
    is_active_hour() {
      current_hour=$(date +"%H")
      if [ ${ACTIVE_HOUR_START} -le ${current_hour} ] && [ ${current_hour} -lt ${ACTIVE_HOUR_END} ]; then
        true
      else
        false
      fi
    }
    
    check_rarity() {
      if [ ${RARITY_ACTIVE_HOUR} -lt ${RARITY_MIN} ]; then
        echo "RARITY_ACTIVE_HOUR should be greater than ${RARITY_MIN}."
      fi
      if [ ${RARITY_NON_ACTIVE_HOUR} -lt ${RARITY_MIN} ]; then
        echo "RARITY_NON_ACTIVE_HOUR should be greater than ${RARITY_MIN}."
      fi
    
      if is_active_hour; then
        echo ${RARITY_ACTIVE_HOUR}
      else
        echo ${RARITY_NON_ACTIVE_HOUR}
      fi
    }
    
    lottery_to_compare() {
      rarity=$(check_rarity)
      num=$(generate_random ${RARITY_MIN} $rarity)
    
      if [ $num = ${RARITY_MIN} ]; then
        true
      else
        false
      fi
    }
    
    if lottery_to_compare; then
      source "${DOTFILES_DIR}/bin/dotfiles_compare.sh"
    fi
    ```

1. (**Macの場合**) シンボリックリンクを作成する。

    ```console
    $ mkdir -p "${HOME}/Library/LaunchAgents"
    $ ln -snfv "${DOTFILES_DIR}/etc/launchd" "${HOME}/Library/LaunchAgents/dotfiles"
    $ ln -snvf "${DOTFILES_DIR}/bin/dotfiles_fetch.sh" /usr/local/bin
    ```

1. (**Ubuntuの場合**) シンボリックリンクを作成する。

    ```console
    $ mkdir -p "${HOME}/.config/systemd/user"
    $ chmod 644 "${DOTFILES_DIR}/etc/systemd/dotfiles-fetch.service"
    $ chmod 644 "${DOTFILES_DIR}/etc/systemd/dotfiles-fetch.timer"
    $ systemctl link --user "${DOTFILES_DIR}/etc/systemd/dotfiles-fetch.service"
    $ systemctl link --user "${DOTFILES_DIR}/etc/systemd/dotfiles-fetch.timer"
    $ ln -snvf "${DOTFILES_DIR}/bin/dotfiles_fetch.sh" /usr/local/bin
    ```

1. (**Macの場合**) `dotfiles-fetch.plist`を起動する。

    ```console
    $ sudo chmod 644 "${HOME}/Library/LaunchAgents/dotfiles/dotfiles-fetch.plist"
    $ launchctl unload "${HOME}/Library/LaunchAgents/dotfiles/dotfiles-fetch.plist" &> /dev/null
    $ launchctl load "${HOME}/Library/LaunchAgents/dotfiles/dotfiles-fetch.plist"
    ```

1. (**Ubuntuの場合**) `dotfiles-fetch.timer`を起動する。

    ```console
    $ systemctl --user daemon-reload
    $ systemctl --user enable "dotfiles-fetch.timer"
    $ systemctl --user start "dotfiles-fetch.timer"
    ```

1. `dotfiles_monitor.zsh`の`RARITY_ACTIVE_HOUR`を1に変更して、shellを起動するタイミングで"Comparing remote dotfiles"というメッセージが標準出力上に出現するか確認する。出現した場合、問題なく設定できているため、`RARITY_ACTIVE_HOUR`を好きな値に設定する。

## 5.5 メリット
- dotfilesをGitHubで管理していたことを思い出すことができる。
- 最新のdotfilesを使用できているか把握することができる。
</div></details>

# 6章 ワンライナーで環境構築
<details><summary>本文</summary><div>

## 6.1 ユースケース
新しいPCでdotfiles環境を手軽に構築したいとき。

## 6.2 解決したい課題
dotfiles環境を構築するのが手間。

## 6.3 アプローチ
環境構築用スクリプトを作成し、curlでダウンロードした上で実行する。
スクリプトの大まかな流れとしては、下記の通り。
他に処理が必要な場合は適宜追加する。

1. dotfiles管理リポジトリのclone。

1. dotfiles管理リポジトリのファイルを使用したシンボリックリンクの作成など、必要とする処理を実行。

## 6.4 実現方法
1. 環境構築用スクリプトを作成する。

    ```shell:install.sh
    #!/bin/bash -e
    DOTFILES_DIR="${HOME}/dotfiles"
    git clone ${Your Remote Repository} ${DOTFILES_DIR}
    # シンボリックリンクの作成など、自分の必要とする処理を記載する
    ```

1. スクリプトをGitHub上にpushする。

1. 下記コマンドでスクリプトをワンライナーで実行する。URLは適宜変更する。下記は`{Yout GitHub Account}/{Your Repository}`上の`{branch}`ブランチに`install.sh`を配置した例。

    ```console
    $ bash -c "$(curl -fsSL https://raw.githubusercontent.com/{Yout GitHub Account}/{Your Repository}/{branch}/install.sh)"
    ```

## 6.5 メリット
お手軽にdotfiles環境構築することができる。

## 参考リンク
- [新しい環境にdotfilesをワンライナーで反映させる](https://tech.kanmu.co.jp/entry/2021/05/20/134419)

</div></details>

# 最後に
本記事に記載した内容でうまく動作しないなどございましたら、是非コメントお願いします。
快適なdotfiles管理生活を送りましょう！