mcpField
X
xdg-base-directory

xdg-base-directory

github.com/jamie-bitflight開発38 回インストール
リポジトリ

xdg-base-directory について

CLI ツールなどのアプリケーションで XDG Base Directory Specification に沿ったパスを設計するためのガイドです。環境変数の扱いやディレクトリの優先順位、Python の実装パターンを説明します。

主な機能

  1. 01設定、データ、状態、キャッシュのパス指針
  2. 02絶対パスと既定値の検証
  3. 03環境変数の優先順位と検索パス
  4. 04Python 標準ライブラリと platformdirs の実装例
  5. 05XDG 準拠のテスト指針

活用シーン

  1. 01CLI ツールのファイル保存先の選定
  2. 02ホームディレクトリの隠しフォルダーからの移行
  3. 03XDG 環境変数のカスタム設定への対応
  4. 04ディレクトリ上書き設定のテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jamie-bitflight/claude_skills --skill xdg-base-directory

インストール後、Claude Code で/xdg-base-directoryを入力して読み込まれたか確認します。

詳細

XDG ベースディレクトリ仕様

XDG ベースディレクトリ仕様に従って、設定、データ、キャッシュ、状態ファイルの標準ディレクトリパスを実装します。

このスキルを使用する場面

次のような場合に、このスキルを使用してください。

  • ユーザー固有のファイルを保存する CLI ツールやアプリケーションを作成する場合
  • Linux/Unix アプリケーションの設定ファイル管理を実装する場合
  • 標準化されたディレクトリパスを必要とするクロスプラットフォームの Python アプリケーションを構築する場合
  • レガシーアプリケーションのパスを ~/.appname から XDG 準拠のパスに移行する場合
  • Python パッケージのファイル保存アーキテクチャを設計する場合
  • ユーザーディレクトリの環境変数を適切に処理する場合
  • ユーザーが設定したディレクトリの設定を尊重するアプリケーションを作成する場合
  • カスタムの XDG ディレクトリによる上書きを使ってアプリケーションをテストする場合

仕様の基本ルール

XDG ベースディレクトリ仕様(バージョン 0.8、2021 年 5 月)では、次の重要な要件が定められています。

1. すべてのパスは絶対パスであること

環境変数の値は絶対パスでなければなりません。相対パスは無効であり、無視しなければなりません。

def validate_xdg_path(path_str: str | None) -> Path | None:
    """Validate XDG path is absolute per specification."""
    if not path_str:
        return None
    path = Path(path_str)
    if not path.is_absolute():
        return None  # Ignore relative paths per spec
    return path
2. 変数が空の場合はデフォルト値を使用する

環境変数が未設定または空の場合は、仕様で定義されたデフォルト値を使用します。

3. 検索パスの優先順位
  • ユーザー固有のディレクトリ($XDG_CONFIG_HOME、$XDG_DATA_HOME)を優先します
  • システムディレクトリ($XDG_CONFIG_DIRS、$XDG_DATA_DIRS)を順に検索します
  • 最初に見つかったものを使用します
4. 検索パスにはコロン区切りを使用する

$XDG_DATA_DIRS と $XDG_CONFIG_DIRS では、$PATH と同様に、パス区切り文字としてコロン(:)を使用します。

環境変数

ユーザー固有のディレクトリ(単一パス)
変数目的デフォルト値用途
$XDG_CONFIG_HOMEユーザー設定ファイル$HOME/.config設定、環境設定、ユーザー設定
$XDG_DATA_HOMEユーザーデータファイル$HOME/.local/shareデータベース、生成コンテンツ、永続データ
$XDG_STATE_HOMEユーザー状態ファイル$HOME/.local/stateログ、履歴、元に戻すバッファー、最近使ったファイル
$XDG_CACHE_HOMEユーザーキャッシュファイル$HOME/.cache一時データ、ダウンロードしたファイル、ビルド成果物
$XDG_RUNTIME_DIRユーザーランタイムファイルシステム設定(/run/user/$UID)ソケット、パイプ、ロックファイル、IPC
システム全体のディレクトリ(検索パス)
変数目的デフォルト値
$XDG_DATA_DIRSシステムデータの検索パス/usr/local/share/:/usr/share/
$XDG_CONFIG_DIRSシステム設定の検索パス/etc/xdg
ユーザー実行ファイル(慣例であり、XDG 仕様ではありません)
パス目的
$HOME/.local/binユーザー固有の実行ファイル

注記:ディストリビューションは、$HOME/.local/bin が $PATH に含まれるようにしてください。

ディレクトリの選択ガイド

データの特性に応じて適切なディレクトリを選択してください。

$XDG_CONFIG_HOME を使用する場合:

  • アプリケーションの設定とプリファレンス
  • ユーザー固有の設定ファイル
  • TOML/YAML/JSON の設定
  • 例:~/.config/myapp/config.toml

$XDG_DATA_HOME を使用する場合:

  • 永続的なアプリケーションデータ
  • ユーザーが作成したコンテンツ
  • ダウンロードしたモデルやアセット
  • データベースファイル
  • 例:~/.local/share/myapp/models/model.gguf

$XDG_STATE_HOME を使用する場合:

  • 操作履歴(コマンド履歴、元に戻すバッファー)
  • 再生成可能なアプリケーションの状態
  • 最近使用したファイルの一覧
  • ユーザー操作に固有のログファイル
  • 例:~/.local/state/myapp/history.log

$XDG_CACHE_HOME を使用する場合:

  • 削除しても問題のない一時ファイル
  • 再取得可能なダウンロードデータ
  • ビルド成果物およびコンパイル済みファイル
  • サムネイルキャッシュ
  • 例:~/.cache/myapp/downloads/

$XDG_RUNTIME_DIR を使用する場合:

  • Unix ドメインソケット
  • 名前付きパイプ(FIFO)
  • ロックファイル
  • 一時的な IPC ファイル
  • 警告:サイズ制限のある tmpfs としてマウントされることが多いため、大きなファイルは避けてください
  • 警告:ログアウト時に削除されるため、永続データを保存しないでください
  • 例:/run/user/1000/myapp/socket

Python の実装パターン

標準ライブラリのみを使用する実装

Linux/Unix システムのみを対象とするアプリケーションや、依存関係を必要としないアプリケーションで使用します。

"""XDG Base Directory compliant path management using stdlib only."""

from pathlib import Path
import os


def get_config_home() -> Path:
    """Get XDG_CONFIG_HOME path.

    Returns user-specific configuration directory.
    Falls back to $HOME/.config if XDG_CONFIG_HOME unset or relative.
    """
    xdg = os.environ.get("XDG_CONFIG_HOME")
    if xdg and Path(xdg).is_absolute():
        return Path(xdg)
    return Path.home() / ".config"


def get_data_home() -> Path:
    """Get XDG_DATA_HOME path.

    Returns user-specific data directory.
    Falls back to $HOME/.local/share if XDG_DATA_HOME unset or relative.
    """
    xdg = os.environ.get("XDG_DATA_HOME")
    if xdg and Path(xdg).is_absolute():
        return Path(xdg)
    return Path.home() / ".local" / "share"


def get_state_home() -> Path:
    """Get XDG_STATE_HOME path.

    Returns user-specific state directory.
    Falls back to $HOME/.local/state if XDG_STATE_HOME unset or relative.
    """
    xdg = os.environ.get("XDG_STATE_HOME")
    if xdg and Path(xdg).is_absolute():
        return Path(xdg)
    return Path.home() / ".local" / "state"


def get_cache_home() -> Path:
    """Get XDG_CACHE_HOME path.

    Returns user-specific cache directory.
    Falls back to $HOME/.cache if XDG_CACHE_HOME unset or relative.
    """
    xdg = os.environ.get("XDG_CACHE_HOME")
    if xdg and Path(xdg).is_absolute():
        return Path(xdg)
    return Path.home() / ".cache"


def get_runtime_dir() -> Path | None:
    """Get XDG_RUNTIME_DIR path.

    Returns user-specific runtime directory, or None if not set.
    This variable has no default - it must be set by the system.
    """
    xdg = os.environ.get("XDG_RUNTIME_DIR")
    if xdg and Path(xdg).is_absolute():
        return Path(xdg)
    return None


def get_config_dirs() -> list[Path]:
    """Get XDG_CONFIG_DIRS as list of paths.

    Returns preference-ordered list of system configuration directories.
    Falls back to [Path('/etc/xdg')] if XDG_CONFIG_DIRS unset.
    """
    xdg = os.environ.get("XDG_CONFIG_DIRS")
    if xdg:
        paths = [Path(p) for p in xdg.split(":") if p and Path(p).is_absolute()]
        if paths:
            return paths
    return [Path("/etc/xdg")]


def get_data_dirs() -> list[Path]:
    """Get XDG_DATA_DIRS as list of paths.

    Returns preference-ordered list of system data directories.
    Falls back to [Path('/usr/local/share'), Path('/usr/share')] if unset.
    """
    xdg = os.environ.get("XDG_DATA_DIRS")
    if xdg:
        paths = [Path(p) for p in xdg.split(":") if p and Path(p).is_absolute()]
        if paths:
            return paths
    return [Path("/usr/local/share"), Path("/usr/share")]
アプリケーション固有のパスモジュール

アプリケーション専用のパスモジュールを作成します。

"""Path management for myapp following XDG specification."""

from pathlib import Path
import os

APP_NAME = "myapp"


def get_config_dir() -> Path:
    """Get myapp config directory."""
    xdg = os.environ.get("XDG_CONFIG_HOME")
    base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / ".config"
    return base / APP_NAME


def get_config_file() -> Path:
    """Get myapp config file path."""
    return get_config_dir() / "config.toml"


def get_data_dir() -> Path:
    """Get myapp data directory."""
    xdg = os.environ.get("XDG_DATA_HOME")
    base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / ".local" / "share"
    return base / APP_NAME


def get_cache_dir() -> Path:
    """Get myapp cache directory."""
    xdg = os.environ.get("XDG_CACHE_HOME")
    base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / ".cache"
    return base / APP_NAME


def get_state_dir() -> Path:
    """Get myapp state directory."""
    xdg = os.environ.get("XDG_STATE_HOME")
    base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / ".local" / "state"
    return base / APP_NAME


def ensure_directories() -> None:
    """Create all required directories with appropriate permissions."""
    for directory in [get_config_dir(), get_data_dir(), get_cache_dir(), get_state_dir()]:
        directory.mkdir(parents=True, exist_ok=True)
platformdirs を使用したクロスプラットフォーム実装

Linux、macOS、Windows を対象とするアプリケーションでは、platformdirs ライブラリを使用します。

"""Cross-platform path management using platformdirs."""

from platformdirs import user_config_dir, user_data_dir, user_cache_dir, user_state_dir
from pathlib import Path

APP_NAME = "myapp"
APP_AUTHOR = "myapp"  # Used on Windows

# Get platform-appropriate directories
# Linux: Uses XDG specification
# macOS: Uses ~/Library/Application Support, ~/Library/Caches
# Windows: Uses %APPDATA%, %LOCALAPPDATA%

config_dir = Path(user_config_dir(APP_NAME, APP_AUTHOR))
data_dir = Path(user_data_dir(APP_NAME, APP_AUTHOR))
cache_dir = Path(user_cache_dir(APP_NAME, APP_AUTHOR))
state_dir = Path(user_state_dir(APP_NAME, APP_AUTHOR))

# Auto-create directories
config_dir = Path(user_config_dir(APP_NAME, APP_AUTHOR, ensure_exists=True))

プラットフォーム固有のパス:

プラットフォーム設定データキャッシュ状態
Linux~/.config/myapp~/.local/share/myapp~/.cache/myapp~/.local/state/myapp
macOS~/Library/Application Support/myapp~/Library/Application Support/myapp~/Library/Caches/myapp~/Library/Application Support/myapp
WindowsC:\Users\<user>\AppData\Local\myapp\myappC:\Users\<user>\AppData\Local\myapp\myappC:\Users\<user>\AppData\Local\myapp\myapp\CacheC:\Users\<user>\AppData\Local\myapp\myapp

platformdirs を使用する場合:

  • 複数のオペレーティングシステムを対象とするアプリケーション
  • プラットフォーム固有の標準規約(macOS の ~/Library、Windows の %APPDATA%)が必要な場合
  • ensure_exists=True によるディレクトリの自動作成を利用したい場合

標準ライブラリのみを使用する場合:

  • Linux/Unix のみを対象とするアプリケーション
  • 依存関係を最小限に抑えたい場合
  • パス解決ロジックを完全に制御する必要がある場合

よくあるアンチパターン

1. ~/.appname の使用(旧式のパターン)

問題:XDG 仕様に違反し、ホームディレクトリが煩雑になります。

# ❌ WRONG
config_file = Path.home() / ".myapp" / "config.toml"

解決策:~/.config/appname/ を使用します。

# ✅ CORRECT
def get_config_dir() -> Path:
    xdg = os.environ.get("XDG_CONFIG_HOME")
    base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / ".config"
    return base / "myapp"


config_file = get_config_dir() / "config.toml"
2. 環境変数を無視する

問題:パスをハードコードすると、ユーザーがカスタマイズできません。

# ❌ WRONG
config_dir = Path.home() / ".config" / "myapp"

解決策:必ず最初に環境変数を確認してください。

# ✅ CORRECT
xdg = os.environ.get("XDG_CONFIG_HOME")
base = Path(xdg) if xdg and Path(xdg).is_absolute() else Path.home() / ".config"
config_dir = base / "myapp"
3. 相対パスを受け入れる

問題:XDG 仕様では絶対パスのみが必須とされています。

# ❌ WRONG
xdg = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
return Path(xdg)  # Accepts relative paths

解決策:絶対パスを検証し、相対パスの場合はデフォルト値を使用します。

# ✅ CORRECT
xdg = os.environ.get("XDG_CONFIG_HOME")
if xdg and Path(xdg).is_absolute():
    return Path(xdg)
return Path.home() / ".config"  # Default for unset or relative
4. ディレクトリを作成しない

問題:親ディレクトリの存在を確認せずにファイルを書き込みます。

# ❌ WRONG
config_file = get_config_dir() / "config.toml"
config_file.write_text(data)  # Fails if directory doesn't exist

解決策:ファイルを書き込む前にディレクトリを作成します。

# ✅ CORRECT
config_file = get_config_dir() / "config.toml"
config_file.parent.mkdir(parents=True, exist_ok=True)
config_file.write_text(data)
5. 設定ディレクトリにキャッシュを保存する

問題:再生成可能なデータと設定が混在します。

# ❌ WRONG
cache_file = get_config_dir() / "download_cache.json"

解決策:再生成可能なデータには $XDG_CACHE_HOME を使用します。

# ✅ CORRECT
cache_file = get_cache_dir() / "download_cache.json"
6. ランタイムディレクトリ内の大きなファイル

問題:$XDG_RUNTIME_DIR は、サイズ制限のある tmpfs としてマウントされることがよくあります。

# ❌ WRONG
large_file = get_runtime_dir() / "model.gguf"  # May be 1GB+

解決策:大きな永続ファイルには $XDG_DATA_HOME を使用します。

# ✅ CORRECT
large_file = get_data_dir() / "models" / "model.gguf"
7. 未設定の XDG_RUNTIME_DIR を処理しない

問題:$XDG_RUNTIME_DIR にデフォルト値はありません。

# ❌ WRONG
runtime_dir = get_runtime_dir()
socket_path = runtime_dir / "socket"  # Fails if None

解決策:使用する前に None かどうかを確認します。

# ✅ CORRECT
runtime_dir = get_runtime_dir()
if runtime_dir is None:
    raise RuntimeError("XDG_RUNTIME_DIR not set by system")
socket_path = runtime_dir / "socket"

XDG 準拠のテスト

環境変数を使った手動テスト
# Test with custom XDG directories
export XDG_CONFIG_HOME=/tmp/test-config
export XDG_DATA_HOME=/tmp/test-data
export XDG_CACHE_HOME=/tmp/test-cache
export XDG_STATE_HOME=/tmp/test-state

# Run application
myapp --help

# Verify files are in correct locations
ls -la /tmp/test-config/myapp/
ls -la /tmp/test-data/myapp/
ls -la /tmp/test-cache/myapp/
ls -la /tmp/test-state/myapp/

# Test with unset variables (should use defaults)
unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME
myapp --help
ls -la ~/.config/myapp/
ls -la ~/.local/share/myapp/
ls -la ~/.cache/myapp/
ls -la ~/.local/state/myapp/
自動テスト
"""Test XDG compliance."""

import os
import tempfile
from pathlib import Path
import pytest


def test_xdg_config_home_override(monkeypatch):
    """XDG_CONFIG_HOME environment variable is respected."""
    with tempfile.TemporaryDirectory() as tmpdir:
        monkeypatch.setenv("XDG_CONFIG_HOME", tmpdir)
        config_dir = get_config_dir()
        assert config_dir == Path(tmpdir) / "myapp"


def test_xdg_config_home_default(monkeypatch):
    """Default used when XDG_CONFIG_HOME unset."""
    monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
    config_dir = get_config_dir()
    assert config_dir == Path.home() / ".config" / "myapp"


def test_relative_path_ignored(monkeypatch):
    """Relative paths in XDG variables are ignored per specification."""
    monkeypatch.setenv("XDG_CONFIG_HOME", "relative/path")
    config_dir = get_config_dir()
    # Should fall back to default, not use relative path
    assert config_dir == Path.home() / ".config" / "myapp"


def test_empty_string_uses_default(monkeypatch):
    """Empty string in XDG variable uses default."""
    monkeypatch.setenv("XDG_CONFIG_HOME", "")
    config_dir = get_config_dir()
    assert config_dir == Path.home() / ".config" / "myapp"


def test_runtime_dir_none_when_unset(monkeypatch):
    """XDG_RUNTIME_DIR returns None when unset (no default)."""
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    runtime_dir = get_runtime_dir()
    assert runtime_dir is None


def test_config_dirs_search_path(monkeypatch):
    """XDG_CONFIG_DIRS parsed as colon-separated search path."""
    monkeypatch.setenv("XDG_CONFIG_DIRS", "/etc/xdg:/opt/config")
    dirs = get_config_dirs()
    assert dirs == [Path("/etc/xdg"), Path("/opt/config")]


def test_data_dirs_ignores_relative_paths(monkeypatch):
    """XDG_DATA_DIRS filters out relative paths."""
    monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share:relative/path:/opt/data")
    dirs = get_data_dirs()
    assert dirs == [Path("/usr/share"), Path("/opt/data")]

ディレクトリ構造の例

~/.config/myapp/              # XDG_CONFIG_HOME
    config.toml               # Main configuration
    credentials.json          # User credentials

~/.local/share/myapp/         # XDG_DATA_HOME
    models/                   # Downloaded models
        model-v1.gguf
    databases/
        user.db

~/.local/state/myapp/         # XDG_STATE_HOME
    history.log               # Command history
    recent-files.json         # Recently used files

~/.local/bin/                 # User binaries (convention)
    myapp                     # Application binary

~/.cache/myapp/               # XDG_CACHE_HOME
    downloads/                # Downloaded temporary files
    build/                    # Build artifacts

設定ファイルの読み込みパターン

XDG に対応した TOML 設定ファイルでは、toml-python スキルを有効にします。

Skill(skill: "python-engineering:toml-python")

toml-python スキルでは、tomllib(Python 3.11 以降)および tomli(バックポート)を使った TOML 解析と、Pydantic モデルによる検証について、包括的なガイダンスを提供します。

関連スキル

関連機能には、次のスキルを有効にしてください。

  • toml-python - TOML 設定ファイルの解析と検証
  • python-engineering - モダンな Python 開発のパターンとベストプラクティス
  • uv - Python パッケージとプロジェクトの管理

参考資料

公式仕様
実装ガイド
コミュニティリソース

主な原則

  1. 絶対パスのみ:すべての XDG パスが絶対パスであることを検証し、相対パスは無視します
  2. 環境変数の優先:デフォルト値より先に、必ず XDG 環境変数を確認します
  3. 仕様への準拠:XDG Base Directory Specification v0.8 に厳密に従います
  4. ディレクトリの作成:ファイルを書き込む前に親ディレクトリを作成します
  5. 適切な保存先:データの種類(設定、データ、キャッシュ、状態)に応じて正しいディレクトリを使用します
  6. ランタイムディレクトリの制限:$XDG_RUNTIME_DIR に大きなファイルを置かないようにします(tmpfs の制限)
  7. クロスプラットフォームへの配慮:macOS/Windows のサポートには platformdirs を使用します
  8. 検索パスの処理:コロン区切りの検索パスを正しく解析します
  9. ランタイムのデフォルト値なし:$XDG_RUNTIME_DIR にデフォルト値はありません。None かどうかを確認します
  10. テスト:環境変数の上書きを使って XDG への準拠を検証します

コメント