glob 模式

一些 PyPA 规范,例如 pyproject.toml 的 license-files,接受特定类型的 glob 模式 来匹配包含通配符和字符范围的给定字符串与文件和目录。本规范定义了哪些模式是可接受的以及应如何处理它们。

有效 glob 模式

出于 PyPA 目的,有效 glob 模式 必须是根据以下规定与文件系统条目匹配的字符串

  • 字母数字字符、下划线 (_)、连字符 (-) 和点 (.) 必须按字面匹配。

  • 特殊 glob 字符:*?** 和字符范围:[] 必须只包含按字面匹配的字符。在 [...] 中,连字符表示不区分区域设置的范围(例如 a-z,顺序基于 Unicode 码点)。开头或结尾的连字符按字面匹配。

  • 路径分隔符必须是正斜杠字符 (/)。

  • 模式始终指 相对路径,例如,当在 pyproject.toml 中使用时,模式应始终相对于包含该文件的目录。因此,不得使用前导斜杠字符。

  • 不得使用父目录指示符 (..)。

本规范未涵盖的任何字符或字符序列都是无效的。项目不得使用此类值。处理 glob 模式的工具应拒绝带有错误的无效值。

字面路径(例如 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