gastownhall/beads · error

unexpected character '!' at position %d (did you mean '!=' o

Error message

unexpected character '!' at position %d (did you mean '!=' or 'NOT'?)

What it means

The lexer encountered a bare '!' that is not immediately followed by '='. The query language only supports '!=' for inequality (or the NOT keyword for logical negation), so a lone '!' is rejected at tokenize time with a hint pointing at both alternatives. This is raised by Lexer.NextToken (internal/query/lexer.go:167) and propagates through Tokenize/Parse.

Source

Thrown at internal/query/lexer.go:167

	if r == 0 {
		return Token{Type: TokenEOF, Pos: startPos}, nil
	}

	switch r {
	case '(':
		return Token{Type: TokenLParen, Value: "(", Pos: startPos}, nil
	case ')':
		return Token{Type: TokenRParen, Value: ")", Pos: startPos}, nil
	case ',':
		return Token{Type: TokenComma, Value: ",", Pos: startPos}, nil
	case '=':
		return Token{Type: TokenEquals, Value: "=", Pos: startPos}, nil
	case '!':
		if l.peek() == '=' {
			l.next()
			return Token{Type: TokenNotEquals, Value: "!=", Pos: startPos}, nil
		}
		return Token{}, fmt.Errorf("unexpected character '!' at position %d (did you mean '!=' or 'NOT'?)", startPos)
	case '<':
		if l.peek() == '=' {
			l.next()
			return Token{Type: TokenLessEq, Value: "<=", Pos: startPos}, nil
		}
		return Token{Type: TokenLess, Value: "<", Pos: startPos}, nil
	case '>':
		if l.peek() == '=' {
			l.next()
			return Token{Type: TokenGreaterEq, Value: ">=", Pos: startPos}, nil
		}
		return Token{Type: TokenGreater, Value: ">", Pos: startPos}, nil
	case '"', '\'':
		return l.readString(r, startPos)
	default:
		if unicode.IsDigit(r) || r == '-' || r == '+' {
			l.backup()
			return l.readNumberOrDuration(startPos)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Add the '=' to make a != comparison: `status!=open`.
  2. Use the NOT keyword for logical negation: `NOT status=open`.
  3. If the '!' is literal data (e.g. in a value), quote it: `title="hello!"`.
  4. Check shell history expansion (bash `!` substitution) that may have mangled the query before it reached bd.

Example fix

// before
query.Parse("status!open")
// after
query.Parse("status!=open")
// or
query.Parse("NOT status=open")
Defensive patterns

Strategy: validation

Validate before calling

func validBangUsage(q string) error {
    for i := 0; i < len(q); i++ {
        if q[i] == '!' && (i+1 >= len(q) || q[i+1] != '=') {
            return fmt.Errorf("bare '!' at %d; use '!=' or NOT", i)
        }
    }
    return nil
}

Try / catch

node, err := query.Parse(q)
if err != nil && strings.Contains(err.Error(), "unexpected character '!'") {
    // surface hint: did you mean '!=' or NOT?
}

Prevention

When it happens

Trigger: Calling query.Parse/NewLexer on a string where '!' appears without a following '=', e.g. `status!open` (missing '='), `!!status=open`, `status = open!`, or a shell-history-style `!` character embedded in an unquoted query string.

Common situations: Users familiar with shell or other DSLs typing `!=` but dropping the '=' (`status!open`); copy-pasting shell commands where `!` triggers history expansion and mangles the query; trying to negate a comparison with `!status=open` instead of `NOT status=open`.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/fbe3bf6ad3f99bcb. Report an issue: GitHub.