{"record":{"id":"4b063da85b1700e4","repo":"fishaudio/fish-speech","slug":"unbalanced-braces-s-pattern","errorCode":null,"errorMessage":"Unbalanced braces: '%s' % pattern","messagePattern":"Unbalanced braces: '(.+?)' % pattern","errorType":"validation","errorClass":"UnbalancedBracesError","httpStatus":null,"severity":"error","filePath":"fish_speech/utils/braceexpand.py","lineNumber":136,"sourceCode":"                # print 'literal:', pattern[start:pos]\n                items.append([pattern[start:pos]])\n                start = pos\n            bracketdepth += 1\n        elif pattern[pos] == \"}\":\n            bracketdepth -= 1\n            if bracketdepth == 0:\n                # print 'expression:', pattern[start+1:pos]\n                expr = pattern[start + 1 : pos]\n                item = parse_expression(expr, escape)\n                if item is None:  # not a range or sequence\n                    items.extend([[\"{\"], parse_pattern(expr, escape), [\"}\"]])\n                else:\n                    items.append(item)\n                start = pos + 1  # skip the closing brace\n        pos += 1\n\n    if bracketdepth != 0:  # unbalanced braces\n        raise UnbalancedBracesError(\"Unbalanced braces: '%s'\" % pattern)\n\n    if start < pos:\n        items.append([pattern[start:]])\n\n    return (\"\".join(item) for item in product(*items))\n\n\ndef parse_expression(expr: str, escape: bool) -> Optional[Iterable[str]]:\n    int_range_match = int_range_re.match(expr)\n    if int_range_match:\n        return make_int_range(*int_range_match.groups())\n\n    char_range_match = char_range_re.match(expr)\n    if char_range_match:\n        return make_char_range(*char_range_match.groups())\n\n    return parse_sequence(expr, escape)\n","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/fishaudio/fish-speech/blob/befe4001745417f8c42131739d862b8a6fdbd15a/fish_speech/utils/braceexpand.py#L118-L154","documentation":"braceexpand's parse_pattern raises UnbalancedBracesError when an opening '{' has no matching closing brace in the pattern. The expansion cannot be computed deterministically so it fails fast.","triggerScenarios":"Calling braceexpand()/parse_pattern() with strings like 'a{1,2' or '{foo' where a brace opens but never closes.","commonSituations":"User-supplied glob/brace patterns from CLI args or config; escaping backslashes consumed by an extra shell/JSON layer removing a literal closing brace.","solutions":["Balance the braces or escape literal braces with backslash: '\\{'","Use escape=True when literal braces should not be expanded","Catch UnbalancedBracesError and surface a clear message to the end user if patterns are user input"],"exampleFix":"# before\nbraceexpand(\"file{1,2.txt\")\n# after\nbraceexpand(\"file\\\\{1,2\\\\}.txt\")  # or \"file{1,2}.txt\"","handlingStrategy":"try-catch","validationCode":"def balanced(s):\n    d = 0\n    for ch in s:\n        if ch == \"{\": d += 1\n        elif ch == \"}\": d -= 1\n        if d < 0: return False\n    return d == 0\nassert balanced(pattern)","typeGuard":null,"tryCatchPattern":"from fish_speech.utils.braceexpand import UnbalancedBracesError\ntry:\n    list(braceexpand(pattern))\nexcept UnbalancedBracesError:\n    pattern = pattern.replace(\"{\", \"\\\\{\").replace(\"}\", \"\\\\}\")","preventionTips":["Validate user patterns before expanding","Prefer escape=True for literal braces"],"tags":["parsing","user-input","brace-expansion"],"backgroundTag":"unbalanced-braces","analyzedSha":"befe4001745417f8c42131739d862b8a6fdbd15a","analyzedAt":"2026-08-27T21:31:45.703Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}