2026/02/19

Nix で Sheldon を使う

Sheldon は Zsh / Bash プラグインマネージャーです

はじめに

キュレェです。

Sheldon は​ Toml を​用いて​設定する​ Zsh / Bash プラグインマネージャーです。

これを​ Nix 環境で​使いたい​とき、​愚直に​やるならば home-manager で​ plugins.toml を​管理すれば​いいわけですが、​home-manager を​使って​管理できるようになったので​ご紹介します。

この​変更は​ home-manager に​俺が​送った​ PR を​引き継いでくれた​ elanora96 さんに​よって​ upstream に​ merge されています。​オープンソースソフトウェアに​おける​暖かさ・​自由さを​知った​一件でした。

Sheldon を​使う​メリット

Zsh に​依存していない

Sheldon は​他の​ Zsh プラグインマネージャとは​違い、​Rust の​ネイティブバイナリで​実装されています。​他の​プラグインマネージャーは​ Zsh Script が​ほとんどである​ため、​Zsh 依存かつ Zsh の​起動時に​評価される​オーバーヘッドが​生じます。

しかし、​Sheldon は​ Zsh に​ stdout 経由で​プラグインを​与えるだけである​ため、​これを​ Zsh が​評価するだけで​ Zsh 側は​プラグインを​読み込むことができます。

テンプレートエンジン

Sheldon の​ examples には​ zsh-defer を​用いた​プラグインの​遅延ロードに​ついての​テンプレートが​書かれています。​これは​事実上 Sheldon ユーザーは​デフォルトで​使う​ものですが、​これが​ Sheldon に​組み込まれておらず​ユーザー側が​定義する​ものと​いう​前提で​ examples に​置かれているのは​以下のような​思想が​あるからだと​邪推します:

  • 特定の​実装に​依存しない

    現在は​ zsh-defer が​ Zsh に​おける​プラグイン遅延ロードの​主流ですが、​主流が​入れ替わった​場合・ユーザーが​別の​メソッドを​使いたい​場合にも​テンプレートを​入れ替えるだけで​容易に​対応する​ことができる

  • Sheldon が​ Zsh 依存ではない

    zsh-defer は​ Zsh 向けだが、​Bash 向けの​設定ならば​当然別の​実装を​使う​必要が​ある。​Sheldon 側で​それを​ラップするのではなく​ユーザーが​テンプレートを​利用するだけで​使い分ける​ことができる

宣言的な​設定

例えば​ Zsh プラグインマネージャの​一つである​ Zinit (妙に​リポジトリの​ owner が​インド人みたいな​名前だった​ことだけ​覚えている​) に​ついては

zinit ice wait lucid atload'...'
zinit light zsh-users/zsh-syntax-highlighting

のように​命令的な​設定を​ .zshrc に​記述して​プラグインを​読みこんでいます。

一方​ Sheldon は​素で​用いる​ならば Toml, home-manager で​設定するなら​ Nix で​宣言的に​プラグインを​読み込むことができます。.zshrc には​ eval "$(sheldon source) を​書くだけで​よいです。

[plugins.zsh-syntax-highlighting]
github = "zsh-users/zsh-syntax-highlighting"
apply = ["defer"]

ローカルパスも​指定できるので、​Nix Store に​置かれている​プラグインを​指定する​ことも​できます。

[plugins.zsh-syntax-highlighting]
local = "/nix/store/xxxxx-source"
apply = ["defer"]

Sheldon を​ Nix で​使う

home-manager の​ programs.sheldon

はじめにで​述べた​とおり、​home-manager には​ programs.sheldon モジュールが​あり、programs.sheldon.settings に​ attrset を​書くと​そのまま​ plugins.toml と​して​生成してくれます。

programs.sheldon = {
  enable = true;
  settings = {
    shell = "zsh";
    plugins = {
      zsh-syntax-highlighting = {
        github = "zsh-users/zsh-syntax-highlighting";
        apply = [ "defer" ];
      };
    };
    templates = {
      defer = ''
        {{ hooks | get: "pre" | nl }}{% for file in files %}zsh-defer source "{{ file }}"
        {% endfor %}{{ hooks | get: "post" | nl }}'';
    };
  };
};

Nix の​式と​して​書けるので、​変数展開や​条件分岐が​そのまま​使えるのが​うれしい​ところです。

プラグインの​ソースを​ Nix で​管理する

Sheldon の​ local ソースを​使えば​プラグインの​取得を​ Nix に​委ねる​ことができます。​flake input と​して​宣言した​ path を​そのまま​渡すだけです。

inputs.fast-syntax-highlighting = {
  url = "github:zdharma-continuum/fast-syntax-highlighting";
  flake = false;
};
plugins.fast-syntax-highlighting = {
  local = "${fast-syntax-highlighting}"; # -> /nix/store/xxxx-source
  apply = [ "defer" ];
};

こうすると​プラグインの​バージ​ョンは​ flake.lock で​管理され、​Sheldon は​ Nix Store の​ディレクトリを​読んで​テンプレートに​流し込むだけの​役割に​なります。

inline プラグインとの​組み合わせ

inline ソースを​使うと​一行の​シェルスクリプトも​プラグインと​して​管理できます。

plugins = {
  compinit = {
    inline = "autoload -U compinit && zsh-defer compinit -C";
  };
  zsh-completions = {
    inline = "fpath+=${zsh-completions}/src";
  };
  zoxide = {
    inline = ''zsh-defer eval "$(zoxide init zsh)"'';
  };
};

.zshrc に​ばらばらに​書いていた​ものが​すべて​ plugins テーブルに​集約されるので、​何が​読み込まれているのかを​ attrset だけで​把握できるようになります。

zsh-defer の​読み込み順に​注意する

zsh-defer は​ Sheldon の​プラグインと​してではなく、.zshrc の​先頭で​ Sheldon より​前に​ source しておく​必要が​あります。​defer テンプレートが​ zsh-defer コマンドを​呼ぶので、sheldon source の​時点で​利用​可能でなければなりません。

{ pkgs }:
let
  load-zsh-defer = ''source "${pkgs.zsh-defer}/share/zsh-defer/zsh-defer.plugin.zsh"'';
  load-sheldon = ''eval "$(sheldon source)"'';
in
{
  programs.zsh = {
    enable = true;
    initContent = pkgs.lib.mkBefore (builtins.concatStringsSep "\n" [
      load-zsh-defer
      load-sheldon
    ]);
  };
}

mkBefore で​ .zshrc の​先頭に​配置して、zsh-defersheldon source の​順を​保証しています。​これを​ミスすると​ defer された​プラグインが​全部​ command not found に​なるので​注意してください​(一敗)

github ソースから​ flake inputs への​移行

ここまでの​例では​ Sheldon の​ github ソースと​ local ソースを​並列に​紹介してきましたが、​実際に​俺が​最初に​書いていた​設定は​ github ソースを​使った​ものでした。

plugins.zsh-syntax-highlighting = {
  github = "zsh-users/zsh-syntax-highlighting";
  apply = [ "defer" ];
};

これは​動くのですが、​プラグインの​取得を​ Sheldon に​任せているので​ Nix の​管理外に​なります。sheldon lock で​生成される​ロックファイルは​ Nix Store の​外に​あり、​再現性の​面では​ flake の​恩恵を​受けられていません。

これを​ flake inputs に​移行するには、​各プラグインを​ flake = false の​ input と​して​宣言し、github を​ local に​書き換えるだけです。

# flake.nix
inputs.zsh-syntax-highlighting = {
  url = "github:zsh-users/zsh-syntax-highlighting";
  flake = false;
};
plugins.zsh-syntax-highlighting = {
  local = "${zsh-syntax-highlighting}"; # github → local に変更
  apply = [ "defer" ];
};

しかし、​プラグインの​数だけルートの​ flake.nix に​ inputs が​増えていきます。​Sheldon 以外の​ inputs も​あるので​これは​結構つらくなります。

Sheldon モジュールを​子 flake に​分離する

そこで​ Sheldon モジュールごと​子 flake に​切り出しました。​子 flake は​独自の​ flake.nix と​ flake.lock を​持つので、​プラグイン用の​ inputs を​ルート flake から​分離できます。

# inputs/sheldon/flake.nix
{
  description = "Sheldon Plugins";
  inputs = {
    zsh-syntax-highlighting = {
      url = "github:zsh-users/zsh-syntax-highlighting";
      flake = false;
    };
    zsh-completions = {
      url = "github:zsh-users/zsh-completions";
      flake = false;
    };
    # ... 他のプラグイン
  };
  outputs = { self, zsh-syntax-highlighting, zsh-completions, ... }: {
    homeManagerModules.default = { ... }@args:
      import ./default.nix (args // {
        inherit zsh-syntax-highlighting zsh-completions;
      });
  };
}

ルートの​ flake.nix からは​ path: で​参照するだけです。

# flake.nix
inputs.sheldon.url = "path:./inputs/sheldon";
# modules/home/default.nix
imports = [
  inputs.sheldon.homeManagerModules.default
];

こうする​ことで、​プラグインの​追加・削除・バージョン更新が​子 flake 内で​完結し、​ルート flake の​ flake.lock を​汚さずに​済むようになりました。​外部​入力を​多く​持つモジュールを​子 flake に​切り出すパターンは​ Sheldon 以外にも​応用できると​思います。

まとめ

  • Sheldon は​ソースの​取得と​ソースの​適用が​分離された​設計に​なっているから​ Nix と​相性が​元々よかったよ!
  • home-manager で​使えるようになったから​おすすめだよ!

ありがとう​ございました。

余談

この​記事が​ヒットしそうな​検索ワードと​して​ Nix Sheldon とか​そういうのが​ありそうですが、​世の​中には​ Sheldon Nix さんが​いるらしいです。​すごい名前。

Reference

この​記事で​紹介した​設定は​俺の​ Nix 設定 (Kyure-A/nix-config) で​実際に​使っている​ものです。