Documentation
¶
Overview ¶
Package filter provides a parser and evaluator for filter query strings used to select Terragrunt components.
Overview ¶
The filter package implements a three-stage compiler architecture:
- Lexer: Tokenizes the input filter query string
- Parser: Builds an Abstract Syntax Tree (AST) from tokens
- Evaluator: Applies the filter logic to discovered Terragrunt components
This design follows the classic compiler pattern and provides a clean separation of concerns between syntax analysis and semantic evaluation.
Filter Syntax ¶
The filter package supports the following syntax elements:
## Path Filters
Path filters match components by their file system path. They support glob patterns:
./apps/frontend # Exact path match ./apps/* # Single-level wildcard ./apps/**/api # Recursive wildcard /absolute/path # Absolute path
## Attribute Filters
Attribute filters match components by their attributes:
name=my-app # Match by config name (directory basename) type=unit # Match components of type "unit" type=stack # Match components of type "stack" external=true # Match external dependencies external=false # Match internal dependencies (not external) foo # Shorthand for name=foo
## Negation Operator (!)
The negation operator excludes matching components:
!name=legacy # Exclude components named "legacy" !./apps/old # Exclude components at path ./apps/old !foo # Exclude components named "foo" !external=true # Exclude external dependencies
## Intersection Operator (|)
The intersection operator refines/narrows results by applying filters from left to right. Each filter in the chain further restricts the results from the previous filter. The pipe character (|) is the only delimiter between filter expressions. Whitespace is optional around operators but is NOT a delimiter itself.
./apps/* | name=web # Components in ./apps/* AND named "web" ./apps/*|name=web # Same as above (spaces optional) ./foo* | !./foobar* # Components in ./foo* AND NOT in ./foobar* type=unit | !external=true # Internal components only
Spaces within component names and paths are preserved:
my app # Component named "my app" (with space) ./my path/file # Path with spaces
## Braced Path Syntax ({})
Use braces to explicitly mark a path expression. This is useful when: - The path doesn't start with ./ or / - You want to be explicit that something is a path, not an identifier
{./apps/*} # Explicitly a path
{my path/file} # Path without ./ prefix
{apps} # Treat "apps" as a path, not a name filter
Operator Precedence ¶
Operators are evaluated with the following precedence (highest to lowest):
- Prefix operators (!)
- Infix operators (| - intersection/refinement, left-to-right)
This means !foo | bar is evaluated as (!foo) | bar, not !(foo | bar). The intersection operator applies filters left-to-right, each filter refining/narrowing the results from the previous filter.
Usage Examples ¶
## Basic Usage
// Parse a filter query
filter, err := filter.Parse("./apps/* | !legacy", ".")
if err != nil {
log.Fatal(err)
}
// Apply the filter to discovered components
// (typically obtained from discovery.Discover())
components := []*component.Component{
{Path: "./apps/app1", Kind: component.Unit},
{Path: "./apps/legacy", Kind: component.Unit},
{Path: "./libs/db", Kind: component.Unit},
}
result, err := filter.Evaluate(components)
if err != nil {
log.Fatal(err)
}
## Multiple Filters (Union)
Multiple filter queries can be combined using the Filters type, which applies union (OR) semantics. This is different from using | within a single filter, which applies intersection (AND) semantics.
// Parse multiple filter queries
filters, err := filter.ParseFilterQueries([]string{
"./apps/*", // Select all apps
"name=db", // OR select db
}, ".")
if err != nil {
log.Fatal(err)
}
result, err := filters.Evaluate(components)
// Returns: all components in ./apps/* OR components named "db"
Multiple filters are evaluated in two phases:
- Positive filters (non-negated) are evaluated and their results are unioned
- Negative filters (starting with !) are applied to remove matching components
The ExcludeByDefault() method signals whether filters operate in exclude-by-default mode. This is true if ANY filter doesn't start with a negation expression:
filters.ExcludeByDefault() // true if any filter is positive
When true, discovery should start with an empty set and add matches. When false (all filters are negated), discovery should start with all components and remove matches.
## One-Shot Usage
// Parse and evaluate in one step
result, err := filter.Apply("./apps/* | name=web", ".", components)
Implementation Details ¶
## Lexer
The lexer (lexer.go) scans the input string and produces tokens:
- IDENT: Identifiers (foo, name, etc.)
- PATH: Paths (./apps/*, /absolute, etc.)
- BANG: Negation operator (!)
- PIPE: Intersection operator (|)
- EQUAL: Assignment operator (=)
- LBRACE: Left brace ({)
- RBRACE: Right brace (})
- EOF: End of input
## Parser
The parser (parser.go) uses recursive descent parsing with Pratt parsing for operators. It produces an AST with the following node types:
- PathFilter: Path/glob filter
- AttributeFilter: Key-value attribute filter
- PrefixExpression: Negation operator
- InfixExpression: Union operator
## Evaluator
The evaluator (evaluator.go) walks the AST and applies the filter logic:
- PathFilter: Uses glob matching (github.com/gobwas/glob) with eager compilation and caching via sync.Once for performance
- AttributeFilter: Matches attributes by key-value pairs:
- name: Matches filepath.Base(component.Path)
- type: Matches component.Kind (unit or stack)
- external: Matches component.External (true or false)
- PrefixExpression: Returns the complement of the right side
- InfixExpression: Returns the intersection by applying right filter to left results
Path filters compile their glob pattern once on first evaluation and cache the compiled result for reuse in subsequent evaluations, providing significant performance improvements when filters are evaluated multiple times.
Related ¶
This package implements the filter syntax described in RFC #4060: https://github.com/gruntwork-io/terragrunt/issues/4060
The syntax is inspired by Turborepo's filter syntax: https://turbo.build/repo/docs/reference/run#--filter-string
Future Enhancements ¶
Future versions will support:
- Git-based filtering ([main...HEAD])
- Dependency traversal (name=foo...)
- Dependents traversal (...name=foo)
- Read-based filtering (reads=path/to/file)
Example (AttributeFilter) ¶
Example_attributeFilter demonstrates filtering components by name attribute.
package main
import (
"fmt"
"github.com/gruntwork-io/terragrunt/internal/component"
"github.com/gruntwork-io/terragrunt/internal/filter"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
func main() {
components := []component.Component{
component.NewUnit("./apps/frontend"),
component.NewUnit("./apps/backend"),
component.NewUnit("./services/api"),
}
l := log.New()
result, _ := filter.Apply(l, "name=api", ".", components)
for _, c := range result {
fmt.Println(c.Path())
}
}
Output: ./services/api
Example (BasicPathFilter) ¶
Example_basicPathFilter demonstrates filtering components by path with a glob pattern.
package main
import (
"fmt"
"path/filepath"
"github.com/gruntwork-io/terragrunt/internal/component"
"github.com/gruntwork-io/terragrunt/internal/filter"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
func main() {
components := []component.Component{
component.NewUnit("./apps/app1"),
component.NewUnit("./apps/app2"),
component.NewUnit("./libs/db"),
}
l := log.New()
result, _ := filter.Apply(l, "./apps/*", ".", components)
for _, c := range result {
fmt.Println(filepath.Base(c.Path()))
}
}
Output: app1 app2
Example (ComplexQuery) ¶
Example_complexQuery demonstrates a complex filter combining paths and negation.
package main
import (
"fmt"
"path/filepath"
"github.com/gruntwork-io/terragrunt/internal/component"
"github.com/gruntwork-io/terragrunt/internal/filter"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
func main() {
components := []component.Component{
component.NewUnit("./services/web"),
component.NewUnit("./services/worker"),
component.NewUnit("./libs/db"),
component.NewUnit("./libs/api"),
component.NewUnit("./libs/cache"),
}
// Select all services except worker
l := log.New()
result, _ := filter.Apply(l, "./services/* | !worker", ".", components)
for _, c := range result {
fmt.Println(filepath.Base(c.Path()))
}
}
Output: web
Example (ErrorHandling) ¶
Example_errorHandling demonstrates handling parsing errors.
package main
import (
"fmt"
"github.com/gruntwork-io/terragrunt/internal/filter"
)
func main() {
// Invalid syntax - missing value after =
_, err := filter.Parse("name=", ".")
if err != nil {
fmt.Println("Error occurred")
}
// Valid syntax
_, err = filter.Parse("name=foo", ".")
if err == nil {
fmt.Println("Successfully parsed")
}
}
Output: Error occurred Successfully parsed
Example (ExclusionFilter) ¶
Example_exclusionFilter demonstrates excluding components using the negation operator.
package main
import (
"fmt"
"path/filepath"
"github.com/gruntwork-io/terragrunt/internal/component"
"github.com/gruntwork-io/terragrunt/internal/filter"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
func main() {
components := []component.Component{
component.NewUnit("./apps/app1"),
component.NewUnit("./apps/app2"),
component.NewUnit("./apps/legacy"),
}
l := log.New()
result, _ := filter.Apply(l, "!legacy", ".", components)
for _, c := range result {
fmt.Println(filepath.Base(c.Path()))
}
}
Output: app1 app2
Example (IntersectionFilter) ¶
Example_intersectionFilter demonstrates refining results with the intersection operator.
package main
import (
"fmt"
"path/filepath"
"github.com/gruntwork-io/terragrunt/internal/component"
"github.com/gruntwork-io/terragrunt/internal/filter"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
func main() {
components := []component.Component{
component.NewUnit("./apps/frontend"),
component.NewUnit("./apps/backend"),
component.NewUnit("./libs/db"),
component.NewUnit("./libs/api"),
}
// Select components in ./apps/ that are named "frontend"
l := log.New()
result, _ := filter.Apply(l, "./apps/* | frontend", ".", components)
for _, c := range result {
fmt.Println(filepath.Base(c.Path()))
}
}
Output: frontend
Example (MultipleFilters) ¶
Example_multipleFilters demonstrates using multiple filters with union semantics.
package main
import (
"fmt"
"path/filepath"
"sort"
"github.com/gruntwork-io/terragrunt/internal/component"
"github.com/gruntwork-io/terragrunt/internal/filter"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
func main() {
components := []component.Component{
component.NewUnit("./apps/app1"),
component.NewUnit("./apps/app2"),
component.NewUnit("./libs/db"),
component.NewUnit("./libs/api"),
}
// Parse multiple filters - results are unioned
filters, _ := filter.ParseFilterQueries([]string{
"./apps/*",
"name=db",
}, ".")
l := log.New()
result, _ := filters.Evaluate(l, components)
// Sort for consistent output
names := make([]string, len(result))
for i, c := range result {
names[i] = filepath.Base(c.Path())
}
sort.Strings(names)
for _, name := range names {
fmt.Println(name)
}
}
Output: app1 app2 db
Example (ParseAndEvaluate) ¶
Example_parseAndEvaluate demonstrates the two-step process of parsing and evaluating.
package main
import (
"fmt"
"github.com/gruntwork-io/terragrunt/internal/component"
"github.com/gruntwork-io/terragrunt/internal/filter"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
func main() {
components := []component.Component{
component.NewUnit("./apps/app1"),
component.NewUnit("./apps/app2"),
}
// Parse the filter once
f, err := filter.Parse("app1", ".")
if err != nil {
fmt.Println("Parse error:", err)
return
}
// Evaluate multiple times with different config sets
l := log.New()
result1, _ := f.Evaluate(l, components)
fmt.Printf("Found %d components\n", len(result1))
// You can also inspect the original query
fmt.Printf("Original query: %s\n", f.String())
}
Output: Found 1 components Original query: app1
Example (RecursiveWildcard) ¶
Example_recursiveWildcard demonstrates using recursive wildcards to match nested paths.
package main
import (
"fmt"
"path/filepath"
"github.com/gruntwork-io/terragrunt/internal/component"
"github.com/gruntwork-io/terragrunt/internal/filter"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
func main() {
components := []component.Component{
component.NewUnit("./infrastructure/networking/vpc"),
component.NewUnit("./infrastructure/networking/subnets"),
component.NewUnit("./infrastructure/compute/app-server"),
}
// Match all infrastructure components at any depth
l := log.New()
result, _ := filter.Apply(l, "./infrastructure/**", ".", components)
for _, c := range result {
fmt.Println(filepath.Base(c.Path()))
}
}
Output: vpc subnets app-server
Index ¶
- Constants
- func Apply(l log.Logger, filterString, workingDir string, components component.Components) (component.Components, error)
- func Evaluate(l log.Logger, expr Expression, components component.Components) (component.Components, error)
- func NewEvaluationError(message string) error
- func NewEvaluationErrorWithCause(message string, cause error) error
- func NewParseError(message string, position int) error
- type AttributeFilter
- type EvaluationError
- type Expression
- type Filter
- type FilterQueryRequiresDiscoveryError
- type Filters
- func (f Filters) Evaluate(l log.Logger, components component.Components) (component.Components, error)
- func (f Filters) EvaluateOnFiles(l log.Logger, files []string) (component.Components, error)
- func (f Filters) HasPositiveFilter() bool
- func (f Filters) RequiresDependencyDiscovery() []Expression
- func (f Filters) RequiresDependentDiscovery() []Expression
- func (f Filters) RequiresDiscovery() (Expression, bool)
- func (f Filters) RequiresParse() (Expression, bool)
- func (f Filters) RestrictToStacks() Filters
- func (f Filters) String() string
- type GraphExpression
- type InfixExpression
- type Lexer
- type ParseError
- type Parser
- type PathFilter
- type PrefixExpression
- type Token
- type TokenType
Examples ¶
Constants ¶
const ( AttributeName = "name" AttributeType = "type" AttributeExternal = "external" AttributeReading = "reading" AttributeSource = "source" AttributeTypeValueUnit = string(component.UnitKind) AttributeTypeValueStack = string(component.StackKind) AttributeExternalValueTrue = "true" AttributeExternalValueFalse = "false" // MaxTraversalDepth is the maximum depth to traverse the graph for both dependencies and dependents. MaxTraversalDepth = 1000000 )
const ( LOWEST int INTERSECTION // | PREFIX // ! )
Operator precedence levels
Variables ¶
This section is empty.
Functions ¶
func Apply ¶
func Apply(l log.Logger, filterString, workingDir string, components component.Components) (component.Components, error)
Apply is a convenience function that parses and evaluates a filter in one step. It's equivalent to calling Parse followed by Evaluate.
func Evaluate ¶
func Evaluate(l log.Logger, expr Expression, components component.Components) (component.Components, error)
Evaluate evaluates an expression against a list of components and returns the filtered components. If logger is provided, it will be used for logging warnings during evaluation.
func NewEvaluationError ¶
NewEvaluationError creates a new EvaluationError with the given message.
func NewEvaluationErrorWithCause ¶
NewEvaluationErrorWithCause creates a new EvaluationError with the given message and cause.
func NewParseError ¶
NewParseError creates a new ParseError with the given message and position.
Types ¶
type AttributeFilter ¶
type AttributeFilter struct {
Key string
Value string
WorkingDir string
// contains filtered or unexported fields
}
AttributeFilter represents a key-value attribute filter (e.g., "name=my-app").
func NewAttributeFilter ¶ added in v0.93.5
func NewAttributeFilter(key string, value string, workingDir string) *AttributeFilter
NewAttributeFilter creates a new AttributeFilter with lazy glob compilation.
func (*AttributeFilter) CompileGlob ¶ added in v0.91.2
func (a *AttributeFilter) CompileGlob() (glob.Glob, error)
CompileGlob returns the compiled glob pattern for name and reading filters, compiling it on first call. Returns an error if called on unsupported attributes (e.g. type, external). Uses sync.Once for thread-safe lazy initialization.
func (*AttributeFilter) IsRestrictedToStacks ¶ added in v0.93.5
func (a *AttributeFilter) IsRestrictedToStacks() bool
func (*AttributeFilter) RequiresDiscovery ¶ added in v0.93.4
func (a *AttributeFilter) RequiresDiscovery() (Expression, bool)
func (*AttributeFilter) RequiresParse ¶ added in v0.93.4
func (a *AttributeFilter) RequiresParse() (Expression, bool)
func (*AttributeFilter) String ¶
func (a *AttributeFilter) String() string
type EvaluationError ¶
EvaluationError represents an error that occurred during evaluation.
func (EvaluationError) Error ¶
func (e EvaluationError) Error() string
type Expression ¶
type Expression interface {
// String returns a string representation of the expression for debugging.
String() string
// RequiresDiscovery returns the first expression that requires discovery of Terragrunt components if any do.
// Additionally, it returns a secondary value of true if any do.
RequiresDiscovery() (Expression, bool)
// RequiresParse returns the first expression that requires parsing Terragrunt HCL configurations if any do.
// Additionally, it returns a secondary value of true if any do.
RequiresParse() (Expression, bool)
// IsRestrictedToStacks returns true if the expression is restricted to stacks.
IsRestrictedToStacks() bool
// contains filtered or unexported methods
}
Expression is the interface that all AST nodes must implement.
type Filter ¶
type Filter struct {
// contains filtered or unexported fields
}
Filter represents a parsed filter query that can be evaluated against discovered configs.
func Parse ¶
Parse parses a filter query string and returns a Filter object. Returns an error if the query cannot be parsed.
func (*Filter) Evaluate ¶
func (f *Filter) Evaluate(l log.Logger, components component.Components) (component.Components, error)
Evaluate applies the filter to a list of components and returns the filtered result. If logger is provided, it will be used for logging warnings during evaluation.
func (*Filter) Expression ¶
func (f *Filter) Expression() Expression
Expression returns the parsed AST expression. This is useful for debugging or advanced use cases.
type FilterQueryRequiresDiscoveryError ¶ added in v0.93.4
type FilterQueryRequiresDiscoveryError struct {
Query string
}
FilterQueryRequiresDiscoveryError is an error that is returned when a filter query requires discovery of Terragrunt configurations.
func (FilterQueryRequiresDiscoveryError) Error ¶ added in v0.93.4
func (e FilterQueryRequiresDiscoveryError) Error() string
type Filters ¶
type Filters []*Filter
Filters represents multiple filter queries that are evaluated with union (OR) semantics. Multiple filters in Filters are always unioned (as opposed to multiple filters within one filter string separated by |, which are intersected).
func ParseFilterQueries ¶
ParseFilterQueries parses multiple filter strings and returns a Filters object. Collects all parse errors and returns them as a joined error if any occur. Returns an empty Filters if filterStrings is empty.
func (Filters) Evaluate ¶
func (f Filters) Evaluate(l log.Logger, components component.Components) (component.Components, error)
Evaluate applies all filters with union (OR) semantics in two phases:
- Positive filters (non-negated) are evaluated and their results are unioned
- Negative filters (starting with negation) are evaluated against the combined results and remove matching components
If logger is provided, it will be used for logging warnings during evaluation.
func (Filters) EvaluateOnFiles ¶ added in v0.92.0
EvaluateOnFiles evaluates the filters on a list of files and returns the filtered result. This is useful for the hcl format command, where we want to evaluate filters on files rather than directories, like we do with components.
func (Filters) HasPositiveFilter ¶ added in v0.91.2
HasPositiveFilter returns true if the filters have any positive filters.
func (Filters) RequiresDependencyDiscovery ¶ added in v0.93.4
func (f Filters) RequiresDependencyDiscovery() []Expression
RequiresDependencyDiscovery returns all target expressions from graph expressions that require dependency traversal.
func (Filters) RequiresDependentDiscovery ¶ added in v0.93.4
func (f Filters) RequiresDependentDiscovery() []Expression
RequiresDependentDiscovery returns all target expressions from graph expressions that require dependent traversal.
func (Filters) RequiresDiscovery ¶ added in v0.93.4
func (f Filters) RequiresDiscovery() (Expression, bool)
RequiresDiscovery returns the first expression that requires discovery of Terragrunt components if any do.
func (Filters) RequiresParse ¶ added in v0.93.4
func (f Filters) RequiresParse() (Expression, bool)
RequiresParse returns the first expression that requires parsing of Terragrunt HCL configurations if any do.
func (Filters) RestrictToStacks ¶ added in v0.93.5
RestrictToStacks returns a new Filters object with only the filters that are restricted to stacks.
type GraphExpression ¶ added in v0.93.4
type GraphExpression struct {
Target Expression
IncludeDependents bool
IncludeDependencies bool
ExcludeTarget bool
}
GraphExpression represents a graph traversal expression (e.g., "...foo", "foo...", "...foo...", "^foo").
func NewGraphExpression ¶ added in v0.93.5
func NewGraphExpression( target Expression, includeDependents bool, includeDependencies bool, excludeTarget bool, ) *GraphExpression
NewGraphExpression creates a new GraphExpression.
func (*GraphExpression) IsRestrictedToStacks ¶ added in v0.93.5
func (g *GraphExpression) IsRestrictedToStacks() bool
func (*GraphExpression) RequiresDiscovery ¶ added in v0.93.4
func (g *GraphExpression) RequiresDiscovery() (Expression, bool)
func (*GraphExpression) RequiresParse ¶ added in v0.93.4
func (g *GraphExpression) RequiresParse() (Expression, bool)
func (*GraphExpression) String ¶ added in v0.93.4
func (g *GraphExpression) String() string
type InfixExpression ¶
type InfixExpression struct {
Left Expression
Right Expression
Operator string
}
InfixExpression represents an infix operator expression (e.g., "./apps/* | name=bar").
func NewInfixExpression ¶ added in v0.93.5
func NewInfixExpression(left Expression, operator string, right Expression) *InfixExpression
NewInfixExpression creates a new InfixExpression.
func (*InfixExpression) IsRestrictedToStacks ¶ added in v0.93.5
func (i *InfixExpression) IsRestrictedToStacks() bool
func (*InfixExpression) RequiresDiscovery ¶ added in v0.93.4
func (i *InfixExpression) RequiresDiscovery() (Expression, bool)
func (*InfixExpression) RequiresParse ¶ added in v0.93.4
func (i *InfixExpression) RequiresParse() (Expression, bool)
func (*InfixExpression) String ¶
func (i *InfixExpression) String() string
type Lexer ¶
type Lexer struct {
// contains filtered or unexported fields
}
Lexer tokenizes a filter query string.
type ParseError ¶
ParseError represents an error that occurred during parsing.
func (ParseError) Error ¶
func (e ParseError) Error() string
type Parser ¶
type Parser struct {
// contains filtered or unexported fields
}
Parser parses a filter query string into an AST.
func (*Parser) ParseExpression ¶
func (p *Parser) ParseExpression() (Expression, error)
ParseExpression parses and returns an expression from the input.
type PathFilter ¶
PathFilter represents a path or glob filter (e.g., "./path/**/*" or "/absolute/path").
func NewPathFilter ¶
func NewPathFilter(value string, workingDir string) *PathFilter
NewPathFilter creates a new PathFilter with lazy glob compilation.
func (*PathFilter) CompileGlob ¶
func (p *PathFilter) CompileGlob() (glob.Glob, error)
CompileGlob returns the compiled glob pattern, compiling it on first call. Subsequent calls return the cached compiled glob and any error. Uses sync.Once for thread-safe lazy initialization.
func (*PathFilter) IsRestrictedToStacks ¶ added in v0.93.5
func (p *PathFilter) IsRestrictedToStacks() bool
func (*PathFilter) RequiresDiscovery ¶ added in v0.93.4
func (p *PathFilter) RequiresDiscovery() (Expression, bool)
func (*PathFilter) RequiresParse ¶ added in v0.93.4
func (p *PathFilter) RequiresParse() (Expression, bool)
func (*PathFilter) String ¶
func (p *PathFilter) String() string
type PrefixExpression ¶
type PrefixExpression struct {
Right Expression
Operator string
}
PrefixExpression represents a prefix operator expression (e.g., "!name=foo").
func NewPrefixExpression ¶ added in v0.93.5
func NewPrefixExpression(operator string, right Expression) *PrefixExpression
NewPrefixExpression creates a new PrefixExpression.
func (*PrefixExpression) IsRestrictedToStacks ¶ added in v0.93.5
func (p *PrefixExpression) IsRestrictedToStacks() bool
func (*PrefixExpression) RequiresDiscovery ¶ added in v0.93.4
func (p *PrefixExpression) RequiresDiscovery() (Expression, bool)
func (*PrefixExpression) RequiresParse ¶ added in v0.93.4
func (p *PrefixExpression) RequiresParse() (Expression, bool)
func (*PrefixExpression) String ¶
func (p *PrefixExpression) String() string
type TokenType ¶
type TokenType int
TokenType represents the type of a token.
const ( // ILLEGAL represents an unknown token ILLEGAL TokenType = iota // EOF represents the end of the input EOF // IDENT represents an identifier (e.g., "foo", "name") IDENT // PATH represents a path (starts with ./ or /) PATH // Operators BANG // negation operator (!) PIPE // intersection operator (|) EQUAL // attribute assignment (=) // Delimiters LBRACE // left brace ({) RBRACE // right brace (}) // Graph operators ELLIPSIS // ellipsis operator (...) CARET // caret operator (^) )