{"id":"27174602f0e8dcc6","repo":"pypa/pip","slug":"unterminated-string-s","errorCode":null,"errorMessage":"unterminated string: %s","messagePattern":"unterminated string: (.+?)","errorType":"validation","errorClass":"SyntaxError","httpStatus":null,"severity":"error","filePath":"src/pip/_vendor/distlib/util.py","lineNumber":94,"sourceCode":"            oq = '\\'\"'.replace(q, '')\n            remaining = remaining[1:]\n            parts = [q]\n            while remaining:\n                # either a string chunk, or oq, or q to terminate\n                if remaining[0] == q:\n                    break\n                elif remaining[0] == oq:\n                    parts.append(oq)\n                    remaining = remaining[1:]\n                else:\n                    m = STRING_CHUNK.match(remaining)\n                    if not m:\n                        raise SyntaxError('error in string literal: %s' % remaining)\n                    parts.append(m.groups()[0])\n                    remaining = remaining[m.end():]\n            else:\n                s = ''.join(parts)\n                raise SyntaxError('unterminated string: %s' % s)\n            parts.append(q)\n            result = ''.join(parts)\n            remaining = remaining[1:].lstrip()  # skip past closing quote\n        return result, remaining\n\n    def marker_expr(remaining):\n        if remaining and remaining[0] == '(':\n            result, remaining = marker(remaining[1:].lstrip())\n            if remaining[0] != ')':\n                raise SyntaxError('unterminated parenthesis: %s' % remaining)\n            remaining = remaining[1:].lstrip()\n        else:\n            lhs, remaining = marker_var(remaining)\n            while remaining:\n                m = MARKER_OP.match(remaining)\n                if not m:\n                    break\n                op = m.groups()[0]","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_vendor/distlib/util.py#L76-L112","documentation":"Raised by distlib's PEP 508 marker parser when a quoted string literal in a marker is opened but never closed before the end of input. The parser's inner loop exits via the `else` clause of the while loop (no closing quote found) and raises `SyntaxError: unterminated string`.","triggerScenarios":"`parse_marker` encounters a marker like `python_version >= '3.8` (missing closing quote). Triggered when pip parses a requirement whose marker has an unbalanced quote.","commonSituations":"Truncated requirement lines; quotes stripped by shell expansion or by copy-paste; line continuations that broke the marker; requirements files edited with an editor that auto-paired quotes incorrectly.","solutions":["Add the missing closing quote that matches the opening one (`'...'` or `\"...\"`).","Avoid shell-splitting marker strings — quote the whole requirement when installing from a shell.","Lint requirements files for balanced quotes."],"exampleFix":"# before\nrequests==2.28.0 ; python_version >= '3.8\n\n# after\nrequests==2.28.0 ; python_version >= '3.8'","handlingStrategy":"validation","validationCode":"from packaging.markers import Marker, InvalidMarker\n\ndef validate_marker_closed(marker: str) -> None:\n    try:\n        Marker(marker)\n    except InvalidMarker as e:\n        raise ValueError(f\"Marker parse error: {e}\") from e","typeGuard":"def marker_quotes_balanced(m: str) -> bool:\n    return m.count(\"'\") % 2 == 0 and m.count('\"') % 2 == 0","tryCatchPattern":"null","preventionTips":["Always close string literals in markers with a matching quote.","Quote the whole requirement in the shell to avoid quote stripping.","Lint for balanced quotes in requirements files."],"tags":["distlib","pep508","marker","requirements-parsing"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}