{"record":{"id":"d118eabdbe523889","repo":"pypa/pip","slug":"invalid-glob-r-recursive-glob-must-be-used","errorCode":null,"errorMessage":"invalid glob %r: recursive glob \"**\" must be used alone","messagePattern":"invalid glob %r: recursive glob \"\\*\\*\" must be used alone","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/pip/_vendor/distlib/util.py","lineNumber":1438,"sourceCode":"                break\n            result /= 1000.0\n        return '%d %sB/s' % (result, unit)\n\n\n#\n# Glob functionality\n#\n\nRICH_GLOB = re.compile(r'\\{([^}]*)\\}')\n_CHECK_RECURSIVE_GLOB = re.compile(r'[^/\\\\,{]\\*\\*|\\*\\*[^/\\\\,}]')\n_CHECK_MISMATCH_SET = re.compile(r'^[^{]*\\}|\\{[^}]*$')\n\n\ndef iglob(path_glob):\n    \"\"\"Extended globbing function that supports ** and {opt1,opt2,opt3}.\"\"\"\n    if _CHECK_RECURSIVE_GLOB.search(path_glob):\n        msg = \"\"\"invalid glob %r: recursive glob \"**\" must be used alone\"\"\"\n        raise ValueError(msg % path_glob)\n    if _CHECK_MISMATCH_SET.search(path_glob):\n        msg = \"\"\"invalid glob %r: mismatching set marker '{' or '}'\"\"\"\n        raise ValueError(msg % path_glob)\n    return _iglob(path_glob)\n\n\ndef _iglob(path_glob):\n    rich_path_glob = RICH_GLOB.split(path_glob, 1)\n    if len(rich_path_glob) > 1:\n        assert len(rich_path_glob) == 3, rich_path_glob\n        prefix, set, suffix = rich_path_glob\n        for item in set.split(','):\n            for path in _iglob(''.join((prefix, item, suffix))):\n                yield path\n    else:\n        if '**' not in path_glob:\n            for item in std_iglob(path_glob):\n                yield item","sourceCodeStart":1420,"sourceCodeEnd":1456,"githubUrl":"https://github.com/pypa/pip/blob/f399c3718970b1b0e2478dac5296eb62679a9b86/src/pip/_vendor/distlib/util.py#L1420-L1456","documentation":"Raised as ValueError by distlib.util.iglob when the recursive glob '**' is not used alone — i.e. it is adjacent to characters other than '/', '\\', ',', or '{'. The regex _CHECK_RECURSIVE_GLOB detects '**' glued directly to other text (like 'a**b' or '**foo'), which the extended globber cannot interpret. The contract is that '**' must be a standalone path component (e.g. 'src/**/tests').","triggerScenarios":"iglob('src/**files'), iglob('foo**bar'), iglob('a/b**'), or any pattern where '**' touches non-separator characters. Correct usage like 'src/**/data' or '**/*.py' is fine because '**' is delimited by '/' or '{}'.","commonSituations":"Building glob patterns dynamically and accidentally concatenating '**' with a token; user-config globs with typos; porting glob patterns from libraries with looser '**' semantics.","solutions":["Ensure '**' is surrounded by path separators: 'src/**/tests' not 'src/**tests'.","Build patterns by joining components with '/' so '**' is always standalone.","Pre-validate with the same _CHECK_RECURSIVE_GLOB regex before calling iglob."],"exampleFix":"# before\niglob('logs/**files')\n# after\niglob('logs/**/files')","handlingStrategy":"validation","validationCode":"from distlib.util import _CHECK_RECURSIVE_GLOB\n\ndef valid_glob(pattern):\n    return not _CHECK_RECURSIVE_GLOB.search(pattern)","typeGuard":"def is_valid_recursive_glob(s: str) -> bool:\n    from distlib.util import _CHECK_RECURSIVE_GLOB\n    return isinstance(s, str) and not _CHECK_RECURSIVE_GLOB.search(s)","tryCatchPattern":"try:\n    list(iglob(pattern))\nexcept ValueError as e:\n    if 'recursive glob' in str(e):\n        pattern = pattern.replace('**', '**/')  # normalize separators","preventionTips":["Always delimit '**' with '/' on both sides.","Build globs by joining components."],"tags":["glob","distlib","filesystem","pattern-matching","recursive-glob"],"backgroundTag":null,"analyzedSha":"f399c3718970b1b0e2478dac5296eb62679a9b86","analyzedAt":"2026-08-08T23:01:42.227Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}