UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

汎用性・拡張性の高い dotfiles 環境を作る

おはこんばんちは!! 尾藤 a.k.a. BTO です。

みなさんターミナルで作業してますか? ターミナルの環境設定はどうやってますか?

dotfiles の環境構築は人それぞれ好みがあるとは思いますが、自動化するのは大前提として、汎用性・拡張性が高いものにしたいですよね。 汎用性が高ければいろんな環境で変更無しで対応できますし、拡張性が高ければ追加や編集がやりやすくなります。

今日は僕が普段使っている dotfiles について紹介します。

僕が普段使っている dotfiles は github で公開しています。

GitHub - bto/dotfiles

このリポジトリで具体的にどのような工夫をしているのかを説明していきます。

使い方

git clone して、make を実行するだけです。 必要なツールのインストールとセットアップを自動でやってくれます。

git clone git@github.com:bto/dotfiles.git
cd dotfiles
make

make update すると、自動で最新の状態にアップデートします。 僕は定期的に make update を叩いて、常に最新の状態で使えるようにしています。

make update

タスクの実行は GNU Make を使う

タスクの実行には GNU Make を使います。 GNU Make を使う理由は、ほとんどの環境でデフォルトでインストールされているという点です。 仮にインストールされてなくても、コマンド一発でインストールできることがほとんどです。

タスク実行ツールはいろいろあれど、インストール率が最も高いのは Make でしょう。 新しいサーバ立てた直後のように、まだ環境が全然整ってない状態で、タスクツールのセットアップを辛いし、全然自動化できてません。 なので、 dotfiles を管理するタスクツールは、インストール率の高さが重要だと思います。

タスクは目的ごとにファイルを分ける

Makefile では、共通の項目だけ設定していて、具体的なタスクは別のファイルをインクルードしています。

Makefile に直接タスクを追加していては、どの行が何に影響しているのかがわかりづらくなります。 目的ごとに別ファイルに分けておけば、ツールごとに必要な設定やタスクが明確になります。 またファイルの追加や削除をするだけで、簡単にタスクを追加したり削除できるようになり、メンテナンス性が上がります。

具体的には、Makefile から config.d/Makefile 以下のファイルをインクルードしています。 目的ごとにディレクトリを作って、変数定義を var.mk に、タスクを task.mk に記述します。

また、いざという時に定義を上書きできるように、config/var.mkconfig/task.mk があった場合にインクルードするようにしています。 この2つは .gitignore に追加していて、リポジトリにコミットしないようにしておきます。

このようにしておくと、特定のサーバで特殊な変更が必要になった時に、リポジトリに影響を与えずに調整することができるようになっています。

タスクの中では、oh-my-zsh のインストールや、*env 系のツールのセットアップ、dotfiles のインストールなんかをやっています。

dotfiles

肝心の dotfiles は rc ディレクトリに入っています。 このディレクトリには、ファイル名から "." を取り除いたファイルが入っています。

ここにあるファイルをホームディレクトリにシンボリックリンクを張るのは、config/Makefile.d/rc のタスクで実行しています。

zshenv

ここから先は zsh での設定です。

zsh の dotfiles はいくつかあるのですが、ホームディレクトリ置くのは、zshenv のみです。 zsh の設定はいろいろあるので、ホームディレクトリにおいてしまうと、どうしてもファイル数が増えてしまいます。 そこで、.zsh というディレクトリ以下に全て置くようにしています。

zsh では、ZDOTDIR という環境変数が設定されていると、そこから dotfiles を読み込んでくれます。 ホームディレクトリの zshenv で設定するのは、ZDOTDIR の設定のみです。

他にも全体で共通で使用する関数の定義をします。 関数は、zsh/function.d の中で個別のファイルに分かれて定義されています。

zshrc

zshrc は単純に zsh/rc.d のファイルを読み込んでいるだけです。

rc.d の中では、主にインタラクティブシェルに関連する項目を設定します。

zshrc にいろいろ設定を追加しすぎて、時間が経った後にどの行が何の設定をしているのかわからなくなったことはないでしょうか。 1つのファイルにたくさんの処理を入れてしまうと、管理が大変になります。 そこで目的ごとに必ず別ファイルにして読み込むことで管理しやすくなるようにしています。

zprofile

zprofile も zshrc と同じように、zsh/profile.d のファイルを読み込みます。

profile.d の中では、環境変数の設定などの設定をしています。

環境変数PATHを自動で設定する

環境変数 PATH ってサーバによって微妙に違ったりしますよね。 そのたびに PATH の設定を変更したりしていないでしょうか。

僕は環境変数 PATH の設定を、サーバの環境に応じてほぼ自動で設定しています。 そのおかげで、サーバごとに設定を変更することがほとんどありません。

考え方は単純で、可能性のあるパスを調べて、ディレクトリだったら追加するという風にしておきます。 また、パスはワイルドカードで指定できるようにしておきます。 こうしておくと、ワイルドカードにマッチしたパスが自動的に PATH に追加されるようになります。

例えば、こんな感じの設定を書いておきます。 _glob_add_path_env() は自分で定義した関数です。

_glob_add_path_env PATH_TMP "/usr/local/*/bin" "/usr/local/*/sbin"

この設定を書いておけば、例えば /usr/local/apache2/bin のようなディレクトリがあったら、自動で PATH に追加されます。 ワイルドカードで指定しているので、/usr/local/mysql-5.6/binのようなディレクトリも存在すれば勝手に追加されます。

マッチするディレクトリが存在しなければ何もしません。

この設定は非常に便利で、初期設定の時間を大幅に短縮してくれるので、非常にオススメです。

コマンドが存在する時だけ設定する

例えば、Mac だと Homebrew を使ったりしますが、当然 Linux 環境には存在しませんよね。 それで、Homebrew が存在する前提の設定を書いていると、当然エラーが出ます。

これを回避する方法は簡単で、コマンドが存在する場合だけ実行すればいいだけです。

例えば、僕の Homebrew の設定には次のように書いています。

_command_exists brew || return

ここで _command_exists() は自分で実装した関数で、引数に渡されたコマンドが存在する場合に、返り値0(つまりTrue)を返します。 brew というコマンドが存在しない場合は、return を実行して後続の処理は実行されないので、Homebrew の無い環境でもエラーが出ません。

ここで単純に return を実行するだけですむのは、ファイルを分割している恩恵でもあります。

このように書いておくと、Homebrew があろうがなかろうが、エラーが出ることはありませんし、Homebrew の設定もちゃんとしてくれるようになります。

.zsh-xxx を読み込むようにする

ここまでの工夫でだいぶいろんな環境に適応できる zsh の dotfiles を作ることができたのですが、これだけで全ての環境に適応することはできません。 なので、そのサーバに独自の調整を追加できるようにしています。

自分のホームディレクトリに .zsh-xxx という(xxxは何でも良い)ディレクトリ以下に、同じディレクトリ構造でファイルを置いておくと、自動で読み込んでくれるようになっています。

僕は細かい調整が必要になった場合は、たいてい .zsh-local というディレクトリを作って、変更したい内容だけ記述するようにしています。 このディレクトリは、当然 dotfiles のリポジトリの管理外にありますので、本体に影響を与えることなく、自由に調整ができるようになっています。

まとめ

  • GNU Make を使うことで、どの環境でもセットアップができるようにした
  • ファイルを目的ごとに分けることで、メンテナンス性と拡張性を向上させた
  • 環境変数 PATH は、ワイルドカード指定でそれぞれのサーバに応じた設定になるようにした
  • 特定のコマンドが存在しない環境でもエラーが起きないように設定した
  • リポジトリ外で設定を上書きできるようにし、サーバごとに細かい調整ができるようにした