kovidgoyal/kitty · error · ParseException

missing )

Error message

missing )

What it means

Raised when a '(' was consumed in a search query's location_expression but the matching ')' is not the next token after the inner expression is parsed. The grammar requires every parenthesized group to be explicitly closed. The error propagates up through not_expression to the top-level parse.

Source

Thrown at kitty/search_query_parser.py:238

            return AndNode(lhs, self.and_expression())

        # Account for the optional 'and'
        if (self.token_type() in (TokenType.WORD, TokenType.QUOTED_WORD) or self.token() == '(') and self.lcase_token() != 'or':
            return AndNode(lhs, self.and_expression())
        return lhs

    def not_expression(self) -> SearchTreeNode:
        if self.lcase_token() == 'not':
            self.advance()
            return NotNode(self.not_expression())
        return self.location_expression()

    def location_expression(self) -> SearchTreeNode:
        if self.token_type() == TokenType.OPCODE and self.token() == '(':
            self.advance()
            res = self.or_expression()
            if self.token_type() != TokenType.OPCODE or self.token(advance=True) != ')':
                raise ParseException(_('missing )'))
            return res
        if self.token_type() not in (TokenType.WORD, TokenType.QUOTED_WORD):
            raise ParseException(_('Invalid syntax. Expected a lookup name or a word'))

        return self.base_token()

    def base_token(self) -> SearchTreeNode:
        if self.token_type() is TokenType.QUOTED_WORD:
            tt = self.token(advance=True)
            assert tt is not None
            if self.allow_no_location:
                return TokenNode('all', tt)
            raise NoLocation(tt)

        tt = self.token(advance=True)
        assert tt is not None
        words = tt.split(':')
        # The complexity here comes from having colon-separated search

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Balance the parentheses: add the missing ')' e.g. '(foo or bar)'
  2. For programmatically built queries, count parens or build the query from a small helper that wraps groups automatically
  3. Test generated queries with kitty.search.query_parser.search() before using them

Example fix

# before
search('(title:foo or title:bar', ...)
# after
search('(title:foo or title:bar)', ...)
Defensive patterns

Strategy: validation

Validate before calling

def balanced(q: str) -> bool:
    depth = 0
    for ch in q:
        if ch == '(':
            depth += 1
        elif ch == ')':
            depth -= 1
            if depth < 0:
                return False
    return depth == 0

Try / catch

try:
    node = build_tree(query, locations)
except ParseException as e:
    if 'missing )' in str(e):
        query = query + ')' * query.count('(') - query.count(')') if False else query  # inspect manually
    raise

Prevention

When it happens

Trigger: Queries like '(foo or bar' (missing close paren), '(foo and (bar' (nested unbalanced), or '(foo ) extra' where the token after the inner expression is not ')'.

Common situations: Dynamically building queries by concatenating fragments and losing a closing parenthesis; hand-written complex queries in kitty's search UI or in templates generating search strings.

Related errors


AI-assisted analysis of kovidgoyal/kitty@6d5d0c4406 (2026-08-27). Data as JSON: /api/errors/c152b119cdeec05f. Report an issue: GitHub.