glob のパターン

PyPA 仕様のいくつか、例えば pyproject.toml の license-files は、あるワイルドカードや文字範囲を含む文字列をファイル名やディレクトリ名にマッチさせるために、ある種の glob パターン を受け付けます。この仕様では、どのようなパターンが受け入れ可能であるのか、や、それらがどのように扱われるべきか、を定義します。

正当な glob パターン

PyPA の目的 <purposes> には、 正当な glob パターン は、以下に指定する通り、ファイルシステム上のエンティティに対して合致する文字列でなければなりません。

  • Alphanumeric characters, spaces (`` ), underscores (``_), hyphens (-), and dots (.) MUST be matched verbatim.

  • glob の特殊文字: *?** 、および、文字範囲: 文字通りにマッチさせる文字を含む [] がサポートされていなければなりません。 [...] の中では、ハイフンは、ロケールに依存しない範囲 (例えば、 Unicode のコードポイントに基づく順序を備えた a-z) を示します。先頭または末尾のハイフンは文字通りにマッチします。

  • パスの区切り文字は、フォワードスラッシュ文字 (/) でなければなりません。

  • パターンは常に 相対パス で参照され、例えば、 pyproject.toml 内で使われるときには、パターンは常にそのファイルを含むディレクトリに対する相対パスであるべきです。したがって、先頭のスラッシュ文字は使ってはなりません。

  • 親ディレクトリ指示子 (..) を使ってはなりません。

この仕様でカバーされていない文字または文字の並びは、すべて不当なものです。プロジェクトは、そのような値を使用してはなりません。glob パターンを消費する <consuming> ツール類は、不当な値をエラーと共に拒絶するべきです。

リテラルのパス (例えば LICENSE) は正当な glob で、これも定義できるということになります。

glob パターンを消費するツール群:

  • それぞれの値を glob パターンであるものとして取り扱わなければならず、また、パターンが不当な glob 文法を含んでいればエラーを発生させなければなりません。

  • 個々のユーザ指定のパターンに合致するファイルが一つもない時は、エラーを発生させなければなりません。

正当な glob パターンの例:

"LICEN[CS]E*"
"AUTHORS*"
"licenses/LICENSE.MIT"
"licenses/LICENSE.CC0"
"LICENSE.txt"
"licenses/*"

不当な glob パターンの例:

"..\LICENSE.MIT"
# .. must not be used.
# \ is an invalid path delimiter, / must be used.

"LICEN{CSE*"
# the { character is not allowed

Python における参照実装

ファイルシステムに対するパターンマッチングの大部分を Python の標準ライブラリの glob モジュールに引き渡すことは可能です。しかしながら、追加の正当性確認を実行することが必要です。

下に示すソースコードは、単純な参照実装としてのものです:

import os
import re
from glob import glob


def find_pattern(pattern: str) -> list[str]:
    """
    >>> find_pattern("/LICENSE.MIT")
    Traceback (most recent call last):
    ...
    ValueError: Pattern '/LICENSE.MIT' should be relative...
    >>> find_pattern("../LICENSE.MIT")
    Traceback (most recent call last):
    ...
    ValueError: Pattern '../LICENSE.MIT' cannot contain '..'...
    >>> find_pattern("LICEN{CSE*")
    Traceback (most recent call last):
    ...
    ValueError: Pattern 'LICEN{CSE*' contains invalid characters...
    """
    if ".." in pattern:
        raise ValueError(f"Pattern {pattern!r} cannot contain '..'")
    if pattern.startswith((os.sep, "/")) or ":\\" in pattern:
        raise ValueError(
            f"Pattern {pattern!r} should be relative and must not start with '/'"
        )
    if re.match(r'^[\w \-\.\/\*\?\[\]]+$', pattern) is None:
        raise ValueError(f"Pattern '{pattern}' contains invalid characters.")
    found = glob(pattern, recursive=True)
    if not found:
        raise ValueError(f"Pattern '{pattern}' did not match any files.")
    return found

歴史

  • January 2025: Initial version

  • March 2026: Treat spaces as a verbatim character