{"record":{"id":"e796f21bc5a529c1","repo":"kovidgoyal/kitty","slug":"extra-characters-at-end-of-search","errorCode":null,"errorMessage":"Extra characters at end of search","messagePattern":"Extra characters at end of search","errorType":"validation","errorClass":"ParseException","httpStatus":null,"severity":"warning","filePath":"kitty/search_query_parser.py","lineNumber":191,"sourceCode":"    def token_type(self) -> TokenType:\n        if self.is_eof():\n            return TokenType.EOF\n        return self.tokens[self.current_token].type\n\n    def is_eof(self) -> bool:\n        return self.current_token >= len(self.tokens)\n\n    def advance(self) -> None:\n        self.current_token += 1\n\n    def tokenize(self, expr: str) -> list[Token]:\n        # Strip out escaped backslashes, quotes and parens so that the\n        # lex scanner doesn't get confused. We put them back later.\n        for k, v in replacements():\n            expr = expr.replace(k, v)\n        tokens, leftover = lex_scanner()(expr)\n        if leftover:\n            raise ParseException(_('Extra characters at end of search'))\n\n        def unescape(x: str) -> str:\n            for k, v in replacements():\n                x = x.replace(v, k[1:])\n            return x\n\n        return [Token(tt, unescape(tv) if tt in (TokenType.WORD, TokenType.QUOTED_WORD) else tv) for tt, tv in tokens]\n\n    def parse(self, expr: str, locations: Sequence[str]) -> SearchTreeNode:\n        self.locations = locations\n        self.tokens = self.tokenize(expr)\n        self.current_token = 0\n        prog = self.or_expression()\n        if not self.is_eof():\n            raise ParseException(_('Extra characters at end of search'))\n        return prog\n\n    def or_expression(self) -> SearchTreeNode:","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/kovidgoyal/kitty/blob/6d5d0c440603ad9bdf6dcd599f73f6dde21acb44/kitty/search_query_parser.py#L173-L209","documentation":"The search-query lexer returns leftover unconsumed input after tokenizing; the parser rejects it with ParseException 'Extra characters at end of search'. Usually unbalanced quotes/parens or stray characters the grammar doesn't accept.","triggerScenarios":"Calling search with an expression like '\"unclosed' or 'foo )' where escaped-unescape round-tripping leaves trailing tokens the scanner could not consume.","commonSituations":"Programmatically building search queries without escaping quotes/parens, or user-typed queries with mismatched delimiters.","solutions":["Balance all quotes and parentheses in the search expression","Escape special characters (backslash, quotes, parens) as the parser expects","Trim trailing junk/whitespace-specials before submitting"],"exampleFix":"# before\nparse_query('\"unclosed')\n# after\nparse_query('\"unclosed\"')","handlingStrategy":"validation","validationCode":"def balanced(q):\n    return q.count('\"')%2==0 and q.count('(')==q.count(')')\nassert balanced(query)","typeGuard":"def is_balanced_query(q: str) -> bool: return q.count('\"')%2==0 and q.count('(')==q.count(')')","tryCatchPattern":"except ParseException: show user a syntax hint and re-prompt","preventionTips":["Escape quotes/parens when composing queries from user input","Test generated queries through the parser before sending to kitty"],"tags":["kitty","search","parsing"],"backgroundTag":"query-parse-error","analyzedSha":"6d5d0c440603ad9bdf6dcd599f73f6dde21acb44","analyzedAt":"2026-08-27T14:20:20.142Z","schemaVersion":2},"datasetVersion":"2026-08-27T19:17:21.184Z"}