最強の dotfiles 駆動開発と GitHub で管理する運用方法

  • 1054
    いいね
  • 7
    コメント

【2015/07/16 追記】優れた dotfiles を設計する - TELLME.TOKYO
この記事では書かなかった全体のロジックについて書きました

Dotfiles Driven Development

dotfiles.gif

dotfiles とは Unix 系 OS で俗に言う設定ファイルのことです。.vimrc.zshrc など、設定ファイルの多くは隠しファイルとしてファイル名の頭にドットがつくことからそう呼ばれています。

ほとんどのエンジニアは CLI 環境での開発は避けては通れないものに思います。CLI 環境は「黒い画面」として敬遠されがちで、CLI になると格段に作業効率がダウンする人も少なく無いです。その作業を効率化するキーとなるのは、設定ファイルの習熟度にあると思います。GUI 開発環境と比べてこちらはテキストベースでカスタマイズできるため、究極まで自分好みに合わせることが可能です。こうした dotfiles のカスタマイズ駆動で開発をすることで効率性は大きく向上します。

また、基本的にソフトウェアの設定は時間的コストと学習コストがかかるものです。これらを失うのは開発効率で大変なディスアドバンテージとなるので、変更履歴の保存とバックアップを兼ねて GitHub で管理するのがマストです。

この dotfiles について管理方法などは様々かと思いますが、個人的には dotfiles の設定はほとんど自動化しています。今回は、dotfiles の運用についての一般的な方法1、と 筆者が dotfiles 関連のタスクをどのように自動化しているのかについて紹介したいと思います。

リポジトリ

筆者のリポジトリは公開しています。

冒頭のスクリーンショットにあるとおり、ワンコマンドで環境構築が可能です。ターミナルを立ち上げて以下のコマンドをコピペするだけです。

bash -c "$(curl -fsSL dot.b4b4r07.com)"

筆者だけに限らず、この手の記事はたくさんリファレンスがあるので、参考として引用しておきます(やはり、普段使いの環境についてなのでそれぞれ拘りを感じます)。

参考:

dotfiles リポジトリの作成手順

(GitHub のアカウント取得から ssh の設定などは趣旨からそれそうなので割愛します)

  1. GitHub にて username/dotfiles というリポジトリ名で作成
  2. mkdir dotfiles && cd dotfiles
  3. ホームディレクトリに転がっているドットファイルを dotfiles/mv する
  4. シンボリックリンク用のスクリプトファイルを用意する
  5. あとは git init から git push まで

以上であなただけの dotfiles リポジトリは完成します。しかし今回は筆者がしばらくこの dotfiles を運用してみてわかったことや便利だと思ったことを後述する運用方法にて説明します。

dotfiles の運用方法

dotfiles リポジトリを作り、開発などをしつつ普段使いの環境の不満などを逐一改善しながら、しばらく運用してきて分かったことがあります。
それは以下のことに沿ってリポジトリを整備することです。

それでは一つずつ見ていきます。

ワンコマンドでインストールできる

GitHub にある dotfiles リポジトリの多くは以下のようなワンコマンドですべてがインストールできるような構成になっていることが多いです。

curl https://{URL}/install.sh | bash

wgetcurl のワンライナーですべての作業(ダウンロード、デプロイ2、イニシャライズ3)が完結できるようにしましょう。たかがターミナルの設定とはいえ、環境の再構築は簡単にできるに越したことはないです。ワンコマンドですぐに再構築できるのは環境が壊れることを恐れさせない強みになります。

ワンコマンドではなく、

git clone https://github.com/username/dotfiles
cd dotfiles
./install.sh

のような決まり文句で導入を促すケースも多いです。やっていることは同じなので、README.md などにワンコマンドでインストール出来るようなコピペ用の記述があると優しいと思われます。

デプロイとイニシャライズは切り分ける

この手の環境セットアップに仕方について大きく二通りあると思っています。

  • デプロイ とは

    多くのソフトウェアはホームディレクトリにあるドットファイルを設定ファイルとして起動時に読み込みます(例:.vimrc)。つまり、ダウンロードした dotfiles リポジトリにあるドットファイルをホームディレクトリにリンクする必要があります。単にファイルをコピーするだけでも同じことですが、シンボリックリンクを貼ることでホームディレクトリにあるドットファイルに追記したり、書き換えたときに自動的に本家(リポジトリでホスティングされているドットファイル)も同期されるので、git push するだけで更新をアップロードすることが出来ます。このリンクを貼ることを便宜上、デプロイと呼ぶことにします。
    デプロイは各ドットファイル毎に ln -s dotfiles_dotfile homedir_dotfile するのはものすごい手間になるので、デプロイ用にスクリプトファイルなどを用意することが多いです。また、このデプロイ用スクリプトは大抵 install.shlink.shsetup.sh といった名前でホスティングされています。

    • 古典的な install.sh

      #!/bin/bash
      
      ln -s .vimrc ~/.vimrc
      ln -s .zshrc ~/.zshrc
      ln -s .tmux.conf ~/.tmux.conf
      

      これでは、新たにドットファイルがリポジトリに追加するたびに install.sh を書き換える必要性があります

    • 一般化した install.sh

      #!/bin/bash
      
      for f in .??*
      do
          [[ "$f" == ".git" ]] && continue
          [[ "$f" == ".DS_Store" ]] && continue
      
          echo "$f"
      done
      

      カレントディレクトリ "." と、親ディレクトリ ".." 以外のドットファイルを正規表現的にリストアップしています。この場合、新しい設定ファイルなどを追加した際に書き換え不要です。その代わり予め、不要なリンクがドットファイルを省く必要があります(continue)。

  • イニシャライズ とは

    イニシャライズとは単にソフトウェアのセットアップ時に必要な初期化処理を指します。dotfiles リポジトリには多くの場合、ドットファイルの他に環境設定用の *.sh ファイルが同梱されていたり、実行を促すような install.sh のオプションやサブコマンドが定義されています(もしくは vim_setup.sh といった具体的な実行ファイルである場合もある)。
    例えば、エディタのカスタムビルドや、パッケージマネージャを通したソフトウェアのインストールやその設定などです。便宜上、これら設定の実行をイニシャライズと呼びます。

このイニシャライズと、デプロイは完全に切り分けるべきです。なぜなら、イニシャライズはその名の通り、リポジトリをダウンロードしたときにしか実行しないからです。一方でデプロイの場合、シンボリックリンクが切れてしまったり、ホストしているリポジトリに新規ファイルがコミットされたときなど、デプロイし直す必要が出てくることが多々あります。
また、どこかのサーバを借りて開発するときなど、大抵の場合は設定ファイルのデプロイさえできればよく、イニシャライズをする必要はありません。

dotfiles のルートディレクトリのディレクトリ構造は簡潔にする

dotfiles のルートディレクトリは GitHub でのファーストビューです。ゴロゴロと規則性もなくディレクトリが多数配置されていては、どこに何があるのかがわかりにくくなり、管理も煩雑化しナンセンスです。UNIX 系 OS の慣習に倣ったディレクトリ命名で役目を明確化させるのがスマートです(例:bin)。

参考:ファイルの命名と整理のルール

実際の dotfiles を例に解説

では上記で説いた理論について、筆者の環境を例に紹介します。

b4b4r07/dotfiles ❤ GitHub

ワンコマンドでインストールできる

 2015-01-16 3.55.33.png

筆者の dotfiles のセットアップは以下のように、curl などのダウンローダを使ってワンコマンドで完了できます。

bash -c "$(curl -L dot.b4b4r07.com)"

筆者の場合、タイプ数を減らすための工夫2をしていますが、

bash -c "$(curl -L raw.githubusercontent.com/b4b4r07/dotfiles/master/etc/install)"

と同じことです。

(注:URL を短くするために dot.b4b4r07.com は GitHub の dotfiles リポジトリにリダイレクトされるようになっています)

このワンライナーが一体何をするかは README.md にもありますが、

  1. リポジトリのダウンロード
  2. ドットファイルのデプロイ
  3. (任意で)イニシャライズ

です。
複数の処理ありますが、ワンライナーをコピペ ENTER するだけで完了するのでめっちゃ簡単です。

dotfiles のプロジェクトページを用意しているので見てみてください。「CURL」か「WGET」のボタンを押すとクリップボードにワンライナーがコピーされます。

デプロイとイニシャライズは切り分ける

筆者の場合、install.shlink.sh のようなデプロイ用のスクリプトファイルの代わりに、Make を使用しています。Make を使うのは環境依存性の排除を重視してのことです。
dotfiles のセットアップ時は、環境が整っていない状態なので、なるべくツールの依存性を少なくしなければなりません。Make であれば、だいたいの Unix ライクシステムでは入っていますし、インストールも簡単です。
環境依存性を少なくするために、Bourne Shell、Make を使うのは王道です。
また、この切り分けについて、Make はプログラムのビルド作業を自動化するツールですが、決まったプロトコルを順番に実行する用途にはもってこいの代物です。これを make deploymake init として利用することで、簡単に切り分けることが出来ます。

Makefile
DOTFILES_EXCLUDES := .DS_Store .git .gitmodules .travis.yml
DOTFILES_TARGET   := $(wildcard .??*) bin
DOTFILES_DIR      := $(PWD)
DOTFILES_FILES    := $(filter-out $(DOTFILES_EXCLUDES), $(DOTFILES_TARGET))

deploy:
    @$(foreach val, $(DOTFILES_FILES), ln -sfnv $(abspath $(val)) $(HOME)/$(val);)

init:
    @$(foreach val, $(wildcard ./etc/init/*.sh), bash $(val);)

ドットファイルとして、$(wildcard .??*) を指定しています。これはワイルドカードによって ... などの要素を除いて、すべてのドットファイルを指定しています。しかし、このままでは関係のないドットファイル(.DS_Store.git など)も含まれてしまうので、ホワイトリスト方式でこれをフィルタリングします。
ドットファイルを一括でターゲットとすることで、新規ドットファイルが追加されてもこの Makefile を修正する必要がないので便利です(ファイルの追加に追従してリンクファイルの修正することを忘れたり、ファイルリストから漏れたりすることで、リンクされないようなバグやそれに関するコンフリクトはよくあります)。

また、イニット用の *.sh ファイルはまとめて、etc/init/ 以下に配置しています。こうすることで、Makefile から foreach でまとめて実行することができます。
ちなみに、etc/init/ には以下の様なスクリプトファイルが保存されています。

  • Vim のカスタムビルド
  • Vim plugins を NeoBundle 経由でインストール
  • ホームディレクトリにあるディレクトリ名の英語化
  • Zsh プラグインマネージャ Antigen のインストール
  • シンタックスハイライタ Pygments のインストール
  • OS X の defaults コマンド群の実行
  • Ruby の開発環境を整備
  • Gem のインストール
  • ...(などの新しい環境で毎回セットアップするうち自動化出来そうなこと)

dotfiles のルートディレクトリのディレクトリ構造は簡潔にする

 2015-01-16 4.02.16.png

筆者の場合、dotfiles のリポジトリルートには以下のディレクトリしか配置していません(ドットファイルなディレクトリを除く)。
やはり、GitHub でのファーストビューにあたるディレクトリ階層は綺麗に役割をもたせた構成にするのが吉です。ただし、役割を分けすぎて第1階層にディレクトリがあふれるのは好ましくないです。見づらさが増すだけです。大別して何個かのディレクトリを作成し、その中で役割のあるディレクトリ構成にしましょう。

  • bin/

    自作のコマンドスクリプトやバイナリなどの保管場所。.zshrc などでは PATH=$PATH:~/bin などとしてパスを通します。ドットファイルに加えて bin/ もデプロイのリストにあるので、ホームディレクトリにリンクされます。make list でデプロイされるファイル・ディレクトリの列挙が可能です。

  • doc/

    ドキュメンテーションがメインですが、ある意味エクストラファイルの保存場所です。設定ファイルでも、自作コマンドでもないもの(マニュアル他、README 用の画像など)が配置されます。

  • etc/

    設定用のスクリプトファイルや、コード関連のファイルの保存場所です。また、etc/init 以下にあるスクリプトは dotfiles のインストール時のイニシャライズに使用されます。doc/ との違いは、コード片か非コード片かだけです。

以上の3ディレクトリと MakefileREADME.md 以外はすべてドットファイルです。見渡し良好でどのディレクトリに何があるか、どんな役割のディレクトリかがすぐに分かります。

各種プラグインについて

Vim や Zsh などプラグインで設定をカスタマイズするソフトウェアも多く、そのプラグインの管理や扱いについても注意すべきでしょう。各種プラグインはリポジトリから切り離すことが必要です。

案1: ただのディレクトリとして取り込む

ダウンロードしたプラグインを、それぞれのプラグインディレクトリ(~/.vim/bundle~/.antigen)にコピーするだけです。しかし、この方法は次のような欠点があります

  • プラグインのアップデートの取得が大変。自分で更新を確認して上書きコピーする必要がある
  • 大きいプラグインの場合、自分のリポジトリのサイズも肥大化する
  • プラグイン数が多いとダウンロードに時間が掛かる

案2: Git のサブモジュールとして取り込む

プラグインは GitHub などで管理されていることが多いので、アップデートなどの更新を取得するために Git のサブモジュールとして取り込む方法です。
しかし、これも自分のリポジトリサイズを大きくしたりダウンロードに時間が掛かるだけでなく、複雑な Git 操作を強いられるため初心者にはお勧めできません。

案3: プラグインマネージャを使用する

Vim も Zsh もそれぞれ、NeoBundleAntigen といった有名なプラグインマネージャがあるのでそれを利用しましょう。

  • Vim

    筆者の場合、Vim の起動時に NeoBundle がなかった場合にのみ実行できる :NeoBundleInit というコマンドを定義しています。

    .vimrc
    let $VIMBUNDLE = '~/.vim/bundle'
    let $NEOBUNDLEPATH = $VIMBUNDLE . '/neobundle.vim'
    if stridx(&runtimepath, $NEOBUNDLEPATH) != -1
    " If the NeoBundle doesn't exist.
    command! NeoBundleInit try | call s:neobundle_init()
                \| catch /^neobundleinit:/
                    \|   echohl ErrorMsg
                    \|   echomsg v:exception
                    \|   echohl None
                    \| endtry
    
    function! s:neobundle_init()
        redraw | echo "Installing neobundle.vim..."
        if !isdirectory($VIMBUNDLE)
            call mkdir($VIMBUNDLE, 'p')
            echo printf("Creating '%s'.", $VIMBUNDLE)
        endif
        cd $VIMBUNDLE
    
        if executable('git')
            call system('git clone git://github.com/Shougo/neobundle.vim')
            if v:shell_error
                throw 'neobundleinit: Git error.'
            endif
        endif
    
        set runtimepath& runtimepath+=$NEOBUNDLEPATH
        call neobundle#rc($VIMBUNDLE)
        try
            echo printf("Reloading '%s'", $MYVIMRC)
            source $MYVIMRC
        catch
            echohl ErrorMsg
            echomsg 'neobundleinit: $MYVIMRC: could not source.'
            echohl None
            return 0
        finally
            echomsg 'Installed neobundle.vim'
        endtry
    
        echomsg 'Finish!'
    endfunction
    
    autocmd! VimEnter * redraw
                \ | echohl WarningMsg
                \ | echo "You should do ':NeoBundleInit' at first!"
                \ | echohl None
    endif
    

    これは、NeoBundle がないときに Vim を起動すると :NeoBundleInit を実行するように促し、すると git clone で NeoBundle をインストールし、そのまま大量のプラグインをインストールします。

    参考:dotfilesをGitHubで管理,vimプラグインをNeoBundleで管理する方法メモ

  • Zsh

    Zsh ではログイン時に起動されるべき項目などを zsh_at_startup として関数化してあります。antigen の初期化や tmux の起動などです。tmux については別記事で詳細に解説してあるのでそちらも参考にしてみてください。

    .zshrc
    antigen_plugins=(
    "brew"
    "zsh-users/zsh-completions"
    "zsh-users/zsh-history-substring-search"
    "zsh-users/zsh-syntax-highlighting"
    "hchbaw/opp.zsh"
    #"tarruda/zsh-autosuggestions"
    "b4b4r07/enhancd"
    "b4b4r07/favdir"
    #"b4b4r07/zsh-vi-mode-visual"
    )
    function zsh_at_startup()
    {   
        # ...(省略)...
    
        tmux_automatically_attach
    
        # Antigen
        if [[ -f ~/.antigen/antigen.zsh ]]; then
            echo -e "=> $fg[blue]Setup antigen....$reset_color"
            local plugin
    
            source ~/.antigen/antigen.zsh
            for plugin in "${antigen_plugins[@]}"
            do
                echo "checking... $plugin"
                antigen bundle "$plugin"
            done
    
            antigen apply
            echo -e "=> $fg[blue]done$reset_color\n"
        fi
    
        # Hello, Zsh!!
        echo -e "\n$fg_bold[cyan]This is ZSH $fg_bold[red]${ZSH_VERSION}$fg_bold[cyan] - DISPLAY on $fg_bold[red]$DISPLAY$reset_color\n"
    }
    if zsh_at_startup; then
        zsh_set_utilities
        zsh_set_prompt
        zsh_set_completion
        zsh_set_setopt
        zsh_set_alias
        zsh_set_keybind
    fi
    

プラグインを自分のリポジトリに取り込む利点は、ワンコマンドで dotfiles をインストールしたときにソフトウェアの設定を完全に再現できることにあります。しかし、プラグインの数が増えるごとに容量も増え、複雑になるのが欠点でした。
そこで重要なのが、シェルやエディタなどの CLI のインフラにあたるところはプラグインなしでも、最低限に快適な作業ができるような設定を心がけるべきです。筆者の場合、エディタに必ず必要と感じているプラグイン(MRU; 最近開いたファイルリストを表示する)を自分製に小型化し、設定ファイルに関数として記述しています。
そして、プラグインはリポジトリから切り離し、それぞれのソフトウェアの中でギリギリまで設定を高めるのがおすすめです。そうすることで、プラグインをインストール出来ない環境や、まったりとダウンロードやインストールが終わるのを待っていられない状況などに即座に開発が開始できます。

まとめ

  • dotfiles のインストールはワンコマンドでする
  • 3ステップのタスク、後者2つの独立性
    • 1.ダウンロード
    • 2.デプロイ
    • 3.イニシャライズ
  • デプロイは簡単にできる
  • プラグインはリポジトリから切り離す
  • そのため設定ファイルでのカスタマイズを怠らない

シェルスクリプトと Makefile

筆者の環境を例に説明してきましたが、筆者の場合はシェルスクリプトと Makefile のみでデプロイやイニシャライズを行っています。しかし、GitHub で「dotfiles」で検索してみると、Rakefile(Ruby)や他のスクリプト言語系(Python、Lua、Prolog)などで行っているケースが多々見られます。OS X などのデフォルトで Ruby ツールがインストールされている環境でのみのセットアップを想定しているのなら問題はないですが、あらゆるほとんどの環境でもセットアップできるようにするためには、なるべく枯れた技術・汎用性の高い技術を使用するべきです。また、シェルスクリプトにも方言やシェルの種類などもあるので、セットアップ用のシェルスクリプトは、汎用性を高めるため POSIX に準拠した /bin/sh で書くのがベターでしょう(それ以外のシェルスクリプトは bash で書いちゃってもいいと思います4)。

参考:

dotfiles の設定は楽しい

冒頭にも述べたとおり、設定ファイルのカスタマイズは CLI 環境の生産性を高めることにつながります。とても便利な CLI ライフが手に入るので、GitHub「dotfiles」でスターの高いリポジトリを参考にしてみたり、筆者の環境 b4b4r07/dotfiles を例にカスタマイズしてみてください。


  1. 一般的な方法 

  2. ドットファイルをホームディレクトリにシンボリックリンクすること; make deploy 

  3. etc/init/ 以下にあるインストールスクリプトを実行すること; make init 

  4. 賛否両論ありますが、bash はデフォルトで多くの環境に採用されスタンダードになりつつあるため、sh で書くことに越したことはないですが、bash で書いても広く汎用性があるといえると筆者は考えていま