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 searchView on GitHub (pinned to 6d5d0c4406)
Solutions
- Balance the parentheses: add the missing ')' e.g. '(foo or bar)'
- For programmatically built queries, count parens or build the query from a small helper that wraps groups automatically
- 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
- Paren-balance check before parsing
- Build nested queries with a helper that opens and closes parens together
- Never hand-edit half of a parenthesized group
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
- Invalid syntax. Expected a lookup name or a word
- Extra characters at end of search
- Failed to parse {query!r}, too much recursion required
- This must be run as kitten ask
- This should be run as kitten broadcast
AI-assisted analysis of kovidgoyal/kitty@6d5d0c4406 (2026-08-27).
Data as JSON: /api/errors/c152b119cdeec05f.
Report an issue: GitHub.