kovidgoyal/kitty · error · ParseException

Invalid syntax. Expected a lookup name or a word

Error message

Invalid syntax. Expected a lookup name or a word

What it means

Raised when the parser reaches a position where a word, quoted word, or '(' was expected but finds something else (an operator like 'and'/'or'/'not' in the wrong place, or a ')' too early). It is the grammar's 'expected a terminal' error in location_expression.

Source

Thrown at kitty/search_query_parser.py:241

        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
        # values. That forces us to check that the first "word" in a colon-
        # separated group is a valid location. If not, then the token must
        # be reconstructed. We also have the problem that locations can be

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Remove or fix dangling operators: never start a query with 'and'/'or', never put two operators in a row
  2. When concatenating optional fragments, filter out empty parts first: ' and '.join(x for x in parts if x)
  3. Avoid empty parentheses '()'

Example fix

# before
parts = [term, optional_filter]
q = ' and '.join(parts)  # optional_filter may be ''
# after
q = ' and '.join(p for p in [term, optional_filter] if p)
Defensive patterns

Strategy: validation

Validate before calling

import re

def wellformed(q: str) -> bool:
    # reject empty groups and dangling operators
    if '()' in q or re.search(r'(^|\s)(and|or|not)(\s|$)*$', q) or re.search(r'^(and|or)(\s|$)', q):
        return False
    return not re.search(r'\b(and|or|not)\s+(and|or|not)\b', q)

Try / catch

try:
    node = build_tree(query, locations)
except ParseException as e:
    show_error(f'Invalid query syntax: {e}')

Prevention

When it happens

Trigger: Queries like 'and foo' (leading operator), 'foo and or bar' (two consecutive operators), 'not and foo', or '()' (empty parentheses).

Common situations: Typos in search queries; concatenating optional filter fragments such that an 'and' is left dangling when one side is empty (e.g. f'{term} and {optional_filter}' with optional_filter='').

Related errors


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