ignore

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 24, 2026 License: MIT Imports: 10 Imported by: 0

README

go-ignore

Go Version Go Reference Go Report Card

A minimal, zero-dependency Go library for matching file paths against .gitignore patterns.

Features

  • Zero dependencies — stdlib only
  • Common gitignore syntax*, ?, **, !, /, trailing /, \ escapes, [abc], [a-z]
  • Nested .gitignore support — scoped base paths
  • Cross-platform — Windows backslash normalization, Unix-correct literal backslash
  • Automatic encoding handling — UTF-8 BOM, CRLF/CR/LF line endings
  • Thread-safe — concurrent access supported
  • Parse warnings — malformed pattern diagnostics
  • Match debuggingMatchWithReason for troubleshooting
  • Configurable — case sensitivity, backtrack limits

Installation

go get github.com/Sriram-PR/go-ignore

Requires Go 1.25 or later.

Quick Start

package main

import (
    "fmt"
    "os"

    ignore "github.com/Sriram-PR/go-ignore"
)

func main() {
    m := ignore.New()

    // IMPORTANT: Add .git/ explicitly if you want Git-like behavior
    // (the library intentionally doesn't auto-ignore .git/)
    m.AddPatterns("", []byte(".git/\n"))

    // Load .gitignore (BOM and CRLF automatically handled)
    content, _ := os.ReadFile(".gitignore")
    if warnings := m.AddPatterns("", content); len(warnings) > 0 {
        for _, w := range warnings {
            fmt.Printf("Warning line %d: %s\n", w.Line, w.Message)
        }
    }

    // Check paths (thread-safe)
    fmt.Println(m.Match("node_modules/foo.js", false)) // true (ignored)
    fmt.Println(m.Match("src/main.go", false))         // false (not ignored)
    fmt.Println(m.Match("build", true))                // true if "build/" pattern exists
}

Usage

Basic Matching
m := ignore.New()
m.AddPatterns("", []byte(`
*.log
build/
node_modules/
!important.log
`))

// Check if path should be ignored
// Second parameter indicates if path is a directory
m.Match("debug.log", false)           // true
m.Match("important.log", false)       // false (negated)
m.Match("build", true)                // true (directory)
m.Match("build/output.js", false)     // true (inside ignored dir)
m.Match("src/main.go", false)         // false
Nested .gitignore Files
m := ignore.New()

// Root .gitignore
m.AddPatterns("", []byte("*.log\n"))

// src/.gitignore (patterns scoped to src/)
m.AddPatterns("src", []byte("*.tmp\n!keep.tmp\n"))

// src/lib/.gitignore (patterns scoped to src/lib/)
m.AddPatterns("src/lib", []byte("*.bak\n"))

// Results:
m.Match("test.log", false)           // true (root pattern)
m.Match("src/test.log", false)       // true (root pattern applies everywhere)
m.Match("src/test.tmp", false)       // true (src pattern)
m.Match("src/keep.tmp", false)       // false (negated in src)
m.Match("test.tmp", false)           // false (src pattern doesn't apply at root)
m.Match("src/lib/test.bak", false)   // true (src/lib pattern)
Debug Why a Path Matches
m := ignore.New()
m.AddPatterns("", []byte(`
*.log
!important.log
build/
`))

result := m.MatchWithReason("debug.log", false)
fmt.Printf("Ignored: %v\n", result.Ignored)   // true
fmt.Printf("Rule: %s\n", result.Rule)         // *.log
fmt.Printf("Line: %d\n", result.Line)         // 1
fmt.Printf("Negated: %v\n", result.Negated)   // false

result = m.MatchWithReason("important.log", false)
fmt.Printf("Ignored: %v\n", result.Ignored)   // false (re-included)
fmt.Printf("Rule: %s\n", result.Rule)         // !important.log
fmt.Printf("Negated: %v\n", result.Negated)   // true
Case-Insensitive Matching (Windows/macOS)
// For case-insensitive filesystems
m := ignore.NewWithOptions(ignore.MatcherOptions{
    CaseInsensitive: true,
})

m.AddPatterns("", []byte("BUILD/\n*.LOG\n"))

m.Match("build", true)      // true
m.Match("Build", true)      // true
m.Match("BUILD", true)      // true
m.Match("test.log", false)  // true
m.Match("test.LOG", false)  // true
Parse Warnings
// Option 1: Collect warnings
m := ignore.New()
warnings := m.AddPatterns("", content)
for _, w := range warnings {
    fmt.Printf("Line %d: %s - %s\n", w.Line, w.Pattern, w.Message)
}

// Option 2: Use a handler (must be set BEFORE AddPatterns)
m := ignore.New()
m.SetWarningHandler(func(basePath string, w ignore.ParseWarning) {
    log.Printf("[%s] line %d: %s", basePath, w.Line, w.Message)
})
m.AddPatterns("", content)       // warnings go to handler
m.AddPatterns("src", srcContent) // warnings include "src" as basePath
Windows Path Support

On Windows, backslashes in paths are automatically normalized to forward slashes. On Linux/macOS, backslashes are treated as literal filename characters (matching Git's behavior).

m := ignore.New()
m.AddPatterns("", []byte("src/build/\n*.log\n"))

// On Windows: backslashes are converted to forward slashes
m.Match("src\\build\\output.exe", false)  // true on Windows
m.Match("src\\main.go", false)            // false on Windows

// On all platforms: forward slashes always work
m.Match("src/build/output.exe", false)    // true
m.Match("src/main.go", false)             // false
Concurrent Usage
m := ignore.New()
m.AddPatterns("", content)

// Safe to call Match from multiple goroutines
var wg sync.WaitGroup
for _, path := range paths {
    wg.Add(1)
    go func(p string) {
        defer wg.Done()
        if m.Match(p, false) {
            // handle ignored file
        }
    }(path)
}
wg.Wait()
Global Gitignore

Load the user's global gitignore file (core.excludesFile or ~/.config/git/ignore) with a single call:

m := ignore.New()

// Load global patterns (core.excludesFile → $XDG_CONFIG_HOME/git/ignore → ~/.config/git/ignore)
if err := m.AddGlobalPatterns(); err != nil {
    log.Fatal(err)
}

// Then load repo-level .gitignore as usual
content, _ := os.ReadFile(".gitignore")
m.AddPatterns("", content)

m.Match("debug.log", false) // may be ignored by global patterns

If the global gitignore file does not exist, AddGlobalPatterns returns nil (no error).

Repository Exclude File

Load the repository's .git/info/exclude file:

m := ignore.New()

// Load .git/info/exclude patterns
if err := m.AddExcludePatterns(".git"); err != nil {
    log.Fatal(err)
}

// Then load repo-level .gitignore as usual
content, _ := os.ReadFile(".gitignore")
m.AddPatterns("", content)

If the exclude file does not exist, AddExcludePatterns returns nil (no error).

Supported Syntax

Pattern Meaning Example Matches
foo File/dir anywhere foo, src/foo, a/b/foo
/foo File/dir at root only foo (not src/foo)
foo/ Directory only foo/ dir and contents
*.log Wildcard extension debug.log, error.log
foo*bar Wildcard middle foobar, fooxyzbar
**/logs Any depth prefix logs, src/logs, a/b/logs
logs/** Everything inside logs/a, logs/a/b/c
a/**/b Any depth middle a/b, a/x/b, a/x/y/z/b
!pattern Negate previous Re-includes matched files
#comment Comment line Ignored
\#file Literal # Matches #file
\!file Literal ! Matches !file
?.txt Single byte a.txt, b.txt (not ab.txt)
[abc] Character class a, b, or c
[a-z] Character range Any lowercase letter
[!abc] Negated class Any char except a, b, c
[[:alpha:]] POSIX class Any letter
\* Literal * Matches * (escaped wildcard)

Note: ? and character classes ([...]) operate on raw bytes, not Unicode code points, consistent with Git's behavior. A multi-byte UTF-8 character requires multiple ? to match.

Pattern Anchoring
  • No slash → matches anywhere: temp matches temp, src/temp, a/b/temp
  • Contains slash → anchored to base: src/temp matches only src/temp
  • Leading slash → anchored to root: /temp matches only temp at root
  • Trailing slash → directories only: build/ matches build/ dir and all contents
  • **/ prefix → floats (not anchored): **/temp matches anywhere

Limitations

The library does not automatically ignore .git/ — add it explicitly if needed.

Path Normalization Notes

The library does not resolve .. (parent directory) components in paths. Paths containing .. are matched literally against patterns. If your application accepts paths from untrusted sources, use filepath.Clean() before passing them to Match() to prevent path traversal issues.

Resource Limits

Default limits prevent resource exhaustion from untrusted input:

Limit Default Description
MaxPatterns 100,000 Total rules a Matcher will hold. Excess rules are dropped with a warning.
MaxPatternLength 4,096 Maximum length of a single pattern line. Longer lines are skipped with a warning.
MaxBacktrackIterations 10,000 Iteration budget shared across all rules per Match call. Prevents pathological ** patterns from causing excessive CPU.

Set any limit to -1 to disable it (not recommended for untrusted input).

API Reference

Types
type Matcher struct { /* ... */ }

type MatcherOptions struct {
    MaxBacktrackIterations int  // Default: 10000, use -1 for unlimited
    CaseInsensitive        bool // Default: false
    MaxPatterns            int  // Default: 100000, use -1 for unlimited
    MaxPatternLength       int  // Default: 4096, use -1 for unlimited
}

type MatchResult struct {
    Ignored  bool   // Final decision
    Matched  bool   // Whether any rule matched
    Rule     string // The matching pattern
    BasePath string // Source .gitignore location
    Line     int    // Line number (1-indexed)
    Negated  bool   // Was it a negation rule
}

type ParseWarning struct {
    Pattern  string
    Message  string
    Line     int
    BasePath string
}

type WarningHandler func(basePath string, warning ParseWarning)
Functions
func New() *Matcher
func NewWithOptions(opts MatcherOptions) *Matcher

func (m *Matcher) AddPatterns(basePath string, content []byte) []ParseWarning
func (m *Matcher) AddGlobalPatterns() error
func (m *Matcher) AddExcludePatterns(gitDir string) error
func (m *Matcher) Match(path string, isDir bool) bool
func (m *Matcher) MatchWithReason(path string, isDir bool) MatchResult
func (m *Matcher) SetWarningHandler(fn WarningHandler)
func (m *Matcher) Warnings() []ParseWarning
func (m *Matcher) RuleCount() int

Performance

Benchmarked on Intel i9-14900HX (Go 1.25, linux/amd64):

Operation Time Allocs
Simple match (hit) ~157ns 0
Simple match (miss) ~214ns 0
Match with ** (shallow) ~174ns 0
Match with ** (deep path) ~3.2µs 0
Match against 200 rules ~9–16µs 0
Pathological ** (bounded) ~920ns–1.3µs 1
Glob matching ~58–91ns 0
Path normalization ~58ns 0

The backtrack budget (MaxBacktrackIterations, default 10,000) is shared across all rules within a single Match call. A matcher with many complex ** patterns will exhaust the budget faster than one with few patterns. When the budget is exceeded, remaining rules are treated as non-matching. Increase the budget via MatcherOptions if needed.

Thread Safety

Matcher is safe for concurrent use:

  • Multiple goroutines can call Match simultaneously (read lock)
  • AddPatterns can be called concurrently with Match (write lock)

Best practice: Batch all AddPatterns calls before starting concurrent Match operations to minimize lock contention.

Contributing

Contributions are welcome! Please ensure:

  1. All tests pass: go test ./...
  2. Race detector passes: go test -race ./...
  3. New features include tests
  4. Code is formatted: go fmt ./...

License

MIT — see LICENSE for details.

Documentation

Overview

Package ignore provides gitignore pattern matching for file paths.

This is a minimal, zero-dependency library for matching file paths against .gitignore patterns. It supports the common gitignore syntax including wildcards (*), double-star (**), negation (!), and directory-only patterns.

Basic Usage

m := ignore.New()

// Add .git/ explicitly if you want Git-like behavior
m.AddPatterns("", []byte(".git/\n"))

// Load .gitignore content
content, _ := os.ReadFile(".gitignore")
m.AddPatterns("", content)

// Check if a path should be ignored
if m.Match("node_modules/foo.js", false) {
    // path is ignored
}

Nested .gitignore Files

For repositories with nested .gitignore files, specify the base path:

// Root .gitignore
m.AddPatterns("", rootContent)

// Nested src/.gitignore
m.AddPatterns("src", srcContent)

Thread Safety

Matcher is safe for concurrent use. Multiple goroutines can call Match simultaneously. AddPatterns can also be called concurrently, though for best performance, batch all AddPatterns calls before concurrent Match calls.

Supported Syntax

The following gitignore patterns are supported:

  • Plain names: "debug.log" matches anywhere in tree
  • Leading /: "/debug.log" matches only at base path
  • Trailing /: "build/" matches directories only
  • Single star: "*.log" matches any .log file
  • Question mark: "?.txt" matches any single byte (not Unicode code point)
  • Double star: "**/logs" matches at any depth
  • Negation: "!important.log" re-includes a file
  • Character classes: "[abc]" matches one byte: a, b, or c
  • Ranges: "[a-z]", "[0-9]" match character ranges
  • Negated classes: "[!abc]" matches any character except a, b, or c
  • POSIX classes: "[[:alpha:]]", "[[:digit:]]" and 10 more
  • Escapes: "\*", "\?", "\#", "\!" for literal matching

Note: ? and [...] operate on raw bytes, not Unicode code points, consistent with Git's behavior.

The backtrack iteration budget (MaxBacktrackIterations, default 10,000) is shared across all rules within a single Match call. This prevents pathological patterns distributed across many rules from causing excessive CPU usage.

Global Gitignore

Load the user's global gitignore file (core.excludesFile or ~/.config/git/ignore) with a single call:

m := ignore.New()
if err := m.AddGlobalPatterns(); err != nil {
    log.Fatal(err)
}

Repository Exclude File

Load the repository's .git/info/exclude file:

if err := m.AddExcludePatterns(".git"); err != nil {
    log.Fatal(err)
}

Path Normalization

Input paths are automatically normalized:

  • Backslashes converted to forward slashes (Windows only)
  • Leading ./ removed
  • Trailing / removed
  • Consecutive slashes collapsed

On Windows, backslash paths work transparently:

m.Match("src\\main.go", false)  // works on Windows

On Linux/macOS, backslashes are valid filename characters and are not converted. Always use forward slashes for portable code.

Package ignore provides gitignore pattern matching for file paths.

Index

Examples

Constants

View Source
const (
	// DefaultMaxPatterns is the maximum number of rules a Matcher will hold.
	// Excess rules are silently dropped with a warning.
	// Set MaxPatterns to 0 to use this default, or -1 for unlimited.
	DefaultMaxPatterns = 100_000

	// DefaultMaxPatternLength is the maximum length of a single pattern line.
	// Lines exceeding this are skipped with a warning.
	// Set MaxPatternLength to 0 to use this default, or -1 for unlimited.
	DefaultMaxPatternLength = 4096
)

Default resource limits for pattern parsing.

View Source
const DefaultMaxBacktrackIterations = 10000

DefaultMaxBacktrackIterations is the default limit for pattern matching iterations. This prevents pathological patterns from causing excessive CPU usage. The budget is shared across all rules for a single Match call and covers both segment-level ** matching and character-level glob matching (*, ?). Can be overridden via MatcherOptions.

Variables

This section is empty.

Functions

This section is empty.

Types

type MatchResult

type MatchResult struct {
	// Rule is the pattern string of the last matching rule (empty if Matched == false).
	// If multiple rules matched, this is the final decisive rule.
	Rule string

	// BasePath is the directory containing the .gitignore that had the matching rule.
	// Empty string means the root .gitignore.
	BasePath string

	// Line is the line number (1-indexed) in the .gitignore file.
	// Zero if Matched == false.
	Line int

	// Ignored indicates the final decision: true if the path should be ignored.
	// This accounts for negation rules.
	Ignored bool

	// Matched indicates whether any rule matched the path (regardless of negation).
	// If false, no rules matched and the path is not ignored (default behavior).
	// If true, at least one rule matched (including negation rules); check Ignored for the final result.
	Matched bool

	// Negated indicates whether the matching rule was a negation (started with !).
	// When Negated == true and Matched == true, the path was re-included.
	Negated bool
}

MatchResult provides detailed information about a match decision.

type Matcher

type Matcher struct {
	// contains filtered or unexported fields
}

Matcher holds compiled gitignore rules.

Thread Safety: Matcher is safe for concurrent use. Concurrent calls to AddPatterns and Match are logically safe and will never cause data races or corruption. However, interleaving AddPatterns with many concurrent Match calls introduces lock contention and may reduce throughput. For best performance, batch all AddPatterns calls before starting concurrent Match operations.

func New

func New() *Matcher

New creates an empty Matcher with default options.

Example
package main

import (
	"fmt"

	ignore "github.com/Sriram-PR/go-ignore"
)

func main() {
	m := ignore.New()
	m.AddPatterns("", []byte("*.log\nbuild/\n!important.log\n"))

	fmt.Println(m.Match("debug.log", false))
	fmt.Println(m.Match("src/main.go", false))
	fmt.Println(m.Match("important.log", false))
	fmt.Println(m.Match("build/output.js", false))
}
Output:
true
false
false
true

func NewWithOptions

func NewWithOptions(opts MatcherOptions) *Matcher

NewWithOptions creates a Matcher with custom options.

Example
package main

import (
	"fmt"

	ignore "github.com/Sriram-PR/go-ignore"
)

func main() {
	m := ignore.NewWithOptions(ignore.MatcherOptions{
		CaseInsensitive: true,
	})
	m.AddPatterns("", []byte("*.LOG\n"))

	fmt.Println(m.Match("debug.log", false))
	fmt.Println(m.Match("DEBUG.LOG", false))
}
Output:
true
true

func (*Matcher) AddExcludePatterns added in v0.3.1

func (m *Matcher) AddExcludePatterns(gitDir string) error

AddExcludePatterns loads patterns from the repository's .git/info/exclude file and adds them to the matcher. The gitDir parameter is the path to the .git directory (e.g., ".git" or an absolute path).

If the exclude file does not exist, AddExcludePatterns returns nil (no error). Only real read failures are returned as errors.

Patterns are added with an empty basePath (root scope), matching Git's behavior where exclude patterns apply to all paths.

Parse warnings are reported through the standard warning mechanism: via the WarningHandler callback if set, otherwise collected and available through Warnings().

Trust model: this function trusts the caller-provided gitDir path and reads the file at gitDir/info/exclude. Callers should ensure gitDir points to a trusted .git directory.

Thread-safe: can be called concurrently with Match.

func (*Matcher) AddGlobalPatterns added in v0.2.5

func (m *Matcher) AddGlobalPatterns() error

AddGlobalPatterns loads the user's global gitignore file and adds its patterns to the matcher. The global gitignore path is resolved in order:

  1. git config --global core.excludesFile (if git is available)
  2. $XDG_CONFIG_HOME/git/ignore (if XDG_CONFIG_HOME is set)
  3. ~/.config/git/ignore (default fallback)

If the resolved file does not exist, AddGlobalPatterns returns nil (no error). Only real read failures are returned as errors.

Patterns are added with an empty basePath (root scope), matching Git's behavior where global patterns apply to all paths.

Parse warnings are reported through the standard warning mechanism: via the WarningHandler callback if set, otherwise collected and available through Warnings().

Trust model: this function trusts the file path returned by "git config" and reads its contents. It should only be called in environments where the git configuration is trusted.

Thread-safe: can be called concurrently with Match.

Example
package main

import (
	"fmt"

	ignore "github.com/Sriram-PR/go-ignore"
)

func main() {
	m := ignore.New()
	if err := m.AddGlobalPatterns(); err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("loaded:", m.RuleCount() >= 0)
}
Output:
loaded: true

func (*Matcher) AddPatterns

func (m *Matcher) AddPatterns(basePath string, content []byte) []ParseWarning

AddPatterns parses gitignore content and adds rules. basePath is the directory containing the .gitignore (empty string for root).

Input normalization (applied automatically):

  • UTF-8 BOM is stripped if present
  • CRLF and CR line endings are normalized to LF
  • Trailing whitespace on each line is trimmed

Both nil and empty content produce no rules. Nil content returns immediately without acquiring locks; empty content goes through parsing (which yields nothing).

Returns warnings for malformed patterns. Warnings are only returned if no WarningHandler was set via SetWarningHandler; otherwise warnings go to the handler.

Thread-safe: can be called concurrently with Match. Performance note: For best performance when loading many .gitignore files, batch AddPatterns calls before starting concurrent Match operations to reduce lock contention.

func (*Matcher) Match

func (m *Matcher) Match(path string, isDir bool) bool

Match returns true if the path should be ignored. path should be relative to repository root using forward slashes. On Windows, backslashes are automatically normalized to forward slashes. On Linux/macOS, backslashes are treated as literal filename characters (matching Git's behavior). isDir indicates whether the path is a directory. Thread-safe: can be called concurrently.

func (*Matcher) MatchWithReason

func (m *Matcher) MatchWithReason(path string, isDir bool) MatchResult

MatchWithReason returns detailed information about why a path matches. Useful for debugging complex .gitignore setups. Thread-safe: can be called concurrently.

Result interpretation:

  • Matched == false: No rules matched; path is not ignored (default)
  • Matched == true, Ignored == true: Path is ignored by Rule
  • Matched == true, Ignored == false: Path was ignored but re-included by negation Rule
Example
package main

import (
	"fmt"

	ignore "github.com/Sriram-PR/go-ignore"
)

func main() {
	m := ignore.New()
	m.AddPatterns("", []byte("*.log\n!important.log\n"))

	result := m.MatchWithReason("debug.log", false)
	fmt.Printf("ignored=%v rule=%q\n", result.Ignored, result.Rule)

	result = m.MatchWithReason("important.log", false)
	fmt.Printf("ignored=%v negated=%v rule=%q\n", result.Ignored, result.Negated, result.Rule)
}
Output:
ignored=true rule="*.log"
ignored=false negated=true rule="!important.log"

func (*Matcher) RuleCount

func (m *Matcher) RuleCount() int

RuleCount returns the number of rules currently loaded. Useful for debugging and testing.

func (*Matcher) SetWarningHandler

func (m *Matcher) SetWarningHandler(fn WarningHandler)

SetWarningHandler sets a callback for parse warnings. If set, warnings are reported via callback instead of being collected. Passing nil resets to collection mode (warnings available via Warnings()). IMPORTANT: Must be called before AddPatterns for the handler to receive warnings. If called after AddPatterns, only subsequent AddPatterns calls will use the handler.

Example
package main

import (
	"fmt"

	ignore "github.com/Sriram-PR/go-ignore"
)

func main() {
	m := ignore.New()
	m.SetWarningHandler(func(basePath string, w ignore.ParseWarning) {
		fmt.Printf("line %d: %s\n", w.Line, w.Message)
	})
	m.AddPatterns("", []byte("*.log\n!\n"))
}
Output:
line 2: pattern is empty after processing

func (*Matcher) Warnings

func (m *Matcher) Warnings() []ParseWarning

Warnings returns all collected parse warnings. Only populated if no WarningHandler was set.

type MatcherOptions

type MatcherOptions struct {
	// MaxBacktrackIterations limits ** pattern matching iterations.
	// Default: DefaultMaxBacktrackIterations (10000).
	// Set to 0 to use default. Set to -1 for unlimited (not recommended).
	MaxBacktrackIterations int

	// CaseInsensitive enables case-insensitive matching.
	// Default: false (case-sensitive, matching Git's default behavior).
	// Note: This affects pattern matching only, not filesystem behavior.
	CaseInsensitive bool

	// MaxPatterns limits the total number of rules a Matcher can hold.
	// Default: DefaultMaxPatterns (100000). Set to -1 for unlimited.
	MaxPatterns int

	// MaxPatternLength limits the length of individual pattern lines.
	// Lines exceeding this limit are skipped with a parse warning.
	// Default: DefaultMaxPatternLength (4096). Set to -1 for unlimited.
	MaxPatternLength int
}

MatcherOptions configures Matcher behavior.

type ParseWarning

type ParseWarning struct {
	Pattern  string // The problematic pattern
	Message  string // Human-readable warning message
	Line     int    // Line number (1-indexed)
	BasePath string // Directory containing the .gitignore (empty for root)
}

ParseWarning represents a warning from parsing a .gitignore line. Warnings are generated for malformed patterns that are skipped during parsing.

type WarningHandler

type WarningHandler func(basePath string, warning ParseWarning)

WarningHandler is called for each parse warning if set.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL