pathrules

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 12 Imported by: 2

README

pathrules

Reusable gitignore-like path rule engine for include/exclude workflows.

Installation

go get github.com/woozymasta/pathrules

Features

  • gitignore-like patterns: *, ?, **, **/, [char-class]
  • optional brace alternation: {a,b,c} (EnableBraceExpansion, off by default)
  • optional backslash escaping of metacharacters: \*, \{, ... (EnableEscaping, off by default, independent of EnableBraceExpansion)
  • leading / anchored rules
  • trailing / directory-only rules
  • ! negation support
  • deterministic last match wins
  • two policy modes:
    • ignore mode (DefaultAction: ActionInclude)
    • allow-list mode (DefaultAction: ActionExclude)

Quick Start

rules, _ := pathrules.ParseRulesString(`
*.tmp
!keep.tmp
`, pathrules.ParseOptions{})

m, _ := pathrules.NewMatcher(rules, pathrules.MatcherOptions{
    DefaultAction: pathrules.ActionInclude,
})

_ = m.Included("keep.tmp", false) // true
_ = m.Included("a.tmp", false)    // false

Custom Parse Options

ParseOptions controls how plain and negated (!-prefixed) lines are mapped to actions, and lets comment/negation prefixes be reconfigured. The default mapping (plain lines exclude, ! includes) is the gitignore convention; inverting it gives an allow-list where plain lines include and ! excludes:

rules, _ := pathrules.ParseRulesString(`
*.c
!*.tmp
`, pathrules.ParseOptions{
    PlainAction:   pathrules.ActionInclude,
    NegatedAction: pathrules.ActionExclude,
})

Other ParseOptions fields: DisableNegation (treat ! as a literal character instead of a prefix), CommentPrefix/NegationPrefix (custom tokens instead of #/!), KeepTrailingSpaces (skip trailing-space trimming).

Brace Alternation

{a,b,c} alternation is opt-in via MatcherOptions.EnableBraceExpansion; disabled by default so { and } stay literal for existing rule sets. Each rule pattern is expanded into its alternatives at compile time (a rule like /CHANGELOG.{md,txt} still counts and matches as one rule):

m, _ := pathrules.NewMatcher([]pathrules.Rule{
    {Action: pathrules.ActionInclude, Pattern: "/README{,.md,.txt}"},
}, pathrules.MatcherOptions{
    DefaultAction:        pathrules.ActionExclude,
    EnableBraceExpansion: true,
})

_ = m.Included("README", false)    // true
_ = m.Included("README.md", false) // true

Empty alternatives ({,.md}) and multiple groups (cartesian product, {foo,bar}-{one,two}) are supported; groups may contain /. Expansion is capped at 256 alternatives per pattern.

The grammar is strict: once enabled, every { must open a complete, non-nested group with at least two comma-separated alternatives that are not all empty (so foo{bar}, foo{}, and foo{,} are all compile errors, not literal text). A literal { then requires EnableEscaping.

Escaping

\X (a literal X) for pattern metacharacters - \*, \?, \[, \], \{, \}, \,, \\ - is opt-in via MatcherOptions.EnableEscaping, independent of EnableBraceExpansion. Disabled by default, so a pattern's backslashes keep being normalized to / (Windows-style path input), same as when this option does not exist:

m, _ := pathrules.NewMatcher([]pathrules.Rule{
    {Action: pathrules.ActionInclude, Pattern: `file\*.txt`},
}, pathrules.MatcherOptions{
    DefaultAction:  pathrules.ActionExclude,
    EnableEscaping: true,
})

_ = m.Included("file*.txt", false) // true, literal "*"
_ = m.Included("fileA.txt", false) // false

Once enabled, use / for path separators instead of \.

Recursive Provider

p, _ := pathrules.NewProvider("/project", pathrules.ProviderOptions{
    RulesFileName: ".pboignore",
    BaseRules: []pathrules.Rule{
        {Action: pathrules.ActionInclude, Pattern: "*.c"},
    },
    MatcherOptions: pathrules.MatcherOptions{
        DefaultAction: pathrules.ActionExclude,
    },
})

ok, _ := p.Included("scripts/main.c", false)
_ = ok

Provider loads rules files from root to target directory, caches compiled matchers, and applies deterministic last-match-wins.

[!IMPORTANT]
for performance, reuse one Provider for the whole directory walk. Creating a new Provider per file forces cold path behavior on every check.

Provider hardening:

  • rejects invalid RulesFileName values (path separators, absolute paths, ..)
  • optional symlink/junction escape check via EnableSymlinkEscapeCheck (disabled by default)

For one-directory batch checks, use DecideInDir / IncludedInDir and pass entry names (DirEntry) instead of calling Decide per file.

Extensions Helper

For workflows that configure only file extensions:

exts := []string{"rvmat", ".paa", "*.ogg"}

rules := pathrules.ParseExtensions(exts)
// []Rule{
//   {Action: ActionInclude, Pattern: "*.rvmat"},
//   {Action: ActionInclude, Pattern: "*.paa"},
//   {Action: ActionInclude, Pattern: "*.ogg"},
// }

Documentation

Overview

Package pathrules implements gitignore-like path matching with reusable include/exclude policies.

The package is intentionally generic and can be used for ignore workflows, allow-list workflows, compression selection, conversion selection, and other path-based pipelines.

Basic flow:

  • parse rules from text (`ParseRules`)
  • optionally load rules from file (`LoadRulesFile`)
  • customize parsing (`ParseOptions`): plain/negated actions, comment and negation prefixes, negation disabling, trailing-space handling
  • optionally enable brace alternation patterns like "*.{md,txt}" (`MatcherOptions.EnableBraceExpansion`, disabled by default)
  • optionally enable backslash escaping of pattern metacharacters like "\*" (`MatcherOptions.EnableEscaping`, disabled by default, independent of EnableBraceExpansion)
  • optionally build extension-based include rules (`ParseExtensions`)
  • compile matcher (`NewMatcher`)
  • ask for decision (`Decide` / `Included` / `Excluded`)

For hierarchical rule files, use `Provider`:

  • create provider with root directory and rules file name
  • evaluate paths relative to that root
  • provider caches compiled directory matchers
  • for one-directory batches use `DecideInDir` / `IncludedInDir`
  • optional symlink/junction escape hardening: `EnableSymlinkEscapeCheck` (disabled by default)

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidRule indicates malformed or unsupported rule input.
	ErrInvalidRule = errors.New("invalid rule")
	// ErrInvalidPattern indicates malformed or unsupported rule pattern.
	ErrInvalidPattern = errors.New("invalid pattern")
	// ErrInvalidParseOptions indicates conflicting or unsupported ParseOptions values.
	ErrInvalidParseOptions = errors.New("invalid parse options")
	// ErrInvalidRulesFileName indicates invalid provider rules file name.
	ErrInvalidRulesFileName = errors.New("invalid rules file name")
	// ErrInvalidEntryName indicates invalid directory entry input for batch APIs.
	ErrInvalidEntryName = errors.New("invalid entry name")
	// ErrNilProvider indicates a nil Provider receiver.
	ErrNilProvider = errors.New("provider is nil")
	// ErrPathOutsideRoot indicates path traversal or non-relative input path.
	ErrPathOutsideRoot = errors.New("path is outside provider root")
	// ErrRulesPathOutsideRoot indicates resolved rules file path escaped provider root.
	ErrRulesPathOutsideRoot = errors.New("rules file path is outside provider root")
)

Sentinel errors for pathrules operations.

Functions

This section is empty.

Types

type Action

type Action uint8

Action represents a decision action of one rule.

const (
	// ActionUnknown is unset/invalid action placeholder.
	ActionUnknown Action = iota
	// ActionExclude means matching path should be excluded.
	ActionExclude
	// ActionInclude means matching path should be included.
	ActionInclude
)

type DirEntry

type DirEntry struct {
	// Name is one entry name relative to target directory (without path separators).
	Name string `json:"name" yaml:"name"`
	// IsDir reports whether entry path is a directory.
	IsDir bool `json:"is_dir,omitempty" yaml:"is_dir,omitempty"`
}

DirEntry is one directory entry input for Provider batch APIs.

type MatchResult

type MatchResult struct {
	// Included reports final include decision.
	Included bool `json:"included" yaml:"included"`
	// Matched reports whether at least one rule matched.
	Matched bool `json:"matched" yaml:"matched"`
	// RuleIndex is the matched rule index in matcher input order, -1 when no match.
	RuleIndex int `json:"rule_index" yaml:"rule_index"`
}

MatchResult is a deterministic decision produced by matcher.

type Matcher

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

Matcher evaluates path decisions against compiled ordered rules.

func NewMatcher

func NewMatcher(rules []Rule, opts MatcherOptions) (*Matcher, error)

NewMatcher compiles ordered rules into matcher.

func (*Matcher) Decide

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

Decide returns deterministic include/exclude decision for one path.

Decision policy:

  • last matched rule wins
  • if no rule matched, default action is used

func (*Matcher) Excluded

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

Excluded reports whether path is excluded by decision policy.

func (*Matcher) Included

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

Included reports whether path is included by decision policy.

type MatcherOptions

type MatcherOptions struct {
	// CaseInsensitive enables ASCII case-insensitive matching.
	CaseInsensitive bool `json:"case_insensitive,omitempty" yaml:"case_insensitive,omitempty"`

	// DefaultAction is applied when no rule matched.
	DefaultAction Action `json:"default_action,omitempty" yaml:"default_action,omitempty"`

	// EnableBraceExpansion enables gitignore-like brace alternation "{a,b,c}" in patterns,
	// expanded into a cartesian product of alternatives at compile time.
	// Disabled by default, so "{" and "}" stay literal for existing rule sets.
	//
	// When enabled, "{" always starts an alternation group and must form a complete,
	// non-nested group with at least two comma-separated alternatives (not all empty);
	// anything else is ErrInvalidPattern. A literal "{" then requires EnableEscaping.
	EnableBraceExpansion bool `json:"enable_brace_expansion,omitempty" yaml:"enable_brace_expansion,omitempty"`

	// EnableEscaping enables backslash-escaping of pattern metacharacters:
	// "\*", "\?", "\[", "\]", "\{", "\}", "\,", "\\",
	// and generically, "\X" for any other character X.
	// Independent of EnableBraceExpansion: useful on its own to match a literal "*" or "?" in a filename.
	//
	// Disabled by default, so a pattern's backslashes keep being normalized to "/"
	// (Windows-style path input), same as when this option does not exist.
	// When enabled, that normalization stops: use "/" for path separators and "\X" for a literal X.
	EnableEscaping bool `json:"enable_escaping,omitempty" yaml:"enable_escaping,omitempty"`
}

MatcherOptions controls matcher behavior.

type ParseOptions added in v0.2.0

type ParseOptions struct {
	// CommentPrefix is the line-comment prefix. Empty defaults to "#".
	// The comment-prefix check always runs before the negation-prefix check,
	// so a line matching both is treated as a comment;
	// do not configure one prefix as a strict prefix of the other (e.g. "#" and "#!").
	CommentPrefix string `json:"comment_prefix,omitempty" yaml:"comment_prefix,omitempty"`
	// NegationPrefix is the negation token. Empty defaults to "!".
	// Only consulted when DisableNegation is false. See CommentPrefix caveat above.
	NegationPrefix string `json:"negation_prefix,omitempty" yaml:"negation_prefix,omitempty"`
	// PlainAction is applied to a plain (non-negated) pattern line.
	// Zero value defaults to ActionExclude.
	PlainAction Action `json:"plain_action,omitempty" yaml:"plain_action,omitempty"`
	// NegatedAction is applied to a negated pattern line.
	// Zero value defaults to ActionInclude. Unused when DisableNegation is true.
	NegatedAction Action `json:"negated_action,omitempty" yaml:"negated_action,omitempty"`
	// DisableNegation disables negation-prefix handling entirely:
	// every line uses PlainAction, and a leading NegationPrefix token
	// (escaped or not) is left in the pattern verbatim.
	DisableNegation bool `json:"disable_negation,omitempty" yaml:"disable_negation,omitempty"`
	// KeepTrailingSpaces skips trailing-space trimming; only trailing "\r" is stripped.
	// Default false preserves current trimming behavior.
	KeepTrailingSpaces bool `json:"keep_trailing_spaces,omitempty" yaml:"keep_trailing_spaces,omitempty"`
}

ParseOptions controls ParseRules line-parsing semantics.

type Provider

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

Provider loads rules files along path hierarchy and evaluates final decisions.

func NewProvider

func NewProvider(rootDir string, opts ProviderOptions) (*Provider, error)

NewProvider creates a recursive rules provider rooted at rootDir.

func (*Provider) Decide

func (p *Provider) Decide(relPath string, isDir bool) (MatchResult, error)

Decide returns final include/exclude decision for a path relative to provider root.

Decision order:

  1. BaseRules matcher.
  2. Rules files from root to deepest containing directory.

Last matched rule wins.

func (*Provider) DecideInDir

func (p *Provider) DecideInDir(relDir string, entries []DirEntry) ([]MatchResult, error)

DecideInDir returns decisions for multiple entries from one directory.

The same directory matcher chain is loaded once and reused for every entry.

func (*Provider) Excluded

func (p *Provider) Excluded(relPath string, isDir bool) (bool, error)

Excluded reports whether path is excluded by provider decision.

func (*Provider) ExcludedInDir

func (p *Provider) ExcludedInDir(relDir string, entries []DirEntry) ([]bool, error)

ExcludedInDir reports exclude decisions for multiple entries from one directory.

func (*Provider) Included

func (p *Provider) Included(relPath string, isDir bool) (bool, error)

Included reports whether path is included by provider decision.

func (*Provider) IncludedInDir

func (p *Provider) IncludedInDir(relDir string, entries []DirEntry) ([]bool, error)

IncludedInDir reports include decisions for multiple entries from one directory.

type ProviderOptions

type ProviderOptions struct {
	// RulesFileName is the rules file loaded in each directory in the path chain.
	// Empty value defaults to ".pathrules".
	RulesFileName string `json:"rules_file_name,omitempty" yaml:"rules_file_name,omitempty"`
	// BaseRules are in-memory rules evaluated before directory-loaded rules.
	BaseRules []Rule `json:"base_rules,omitempty" yaml:"base_rules,omitempty"`
	// ParseOptions controls how each directory's rules file is parsed.
	ParseOptions ParseOptions `json:"parse_options" yaml:"parse_options"`
	// MatcherOptions controls rule matching behavior for all compiled matchers.
	MatcherOptions MatcherOptions `json:"matcher_options" yaml:"matcher_options"`
	// EnableSymlinkEscapeCheck enables resolved-path validation to block
	// symlink/junction escapes outside provider root.
	// Default is false for lower cold-path overhead.
	EnableSymlinkEscapeCheck bool `json:"enable_symlink_escape_check,omitempty" yaml:"enable_symlink_escape_check,omitempty"`
}

ProviderOptions configures recursive rules provider behavior.

type Rule

type Rule struct {
	// Pattern is a gitignore-like pattern.
	Pattern string `json:"pattern" yaml:"pattern"`
	// Action is a decision action applied when the rule matches.
	Action Action `json:"action" yaml:"action"`
}

Rule is one user-visible path rule.

func LoadRulesFile

func LoadRulesFile(path string, opts ParseOptions) ([]Rule, error)

LoadRulesFile reads and parses rules from a file according to opts.

func LoadRulesFiles

func LoadRulesFiles(opts ParseOptions, paths ...string) ([]Rule, error)

LoadRulesFiles reads and merges rules from files in the given order, using opts for every file.

Returned rules preserve file order and rule order inside each file.

func MergeRules

func MergeRules(ruleSets ...[]Rule) []Rule

MergeRules merges rule slices preserving input order.

func ParseExtensions added in v0.1.2

func ParseExtensions(exts []string) []Rule

ParseExtensions converts extension list to include rules.

Accepted extension forms:

  • "txt"
  • ".txt"
  • "*.txt"

Empty values are skipped. Returned patterns are normalized to lower-case "*.ext" form and preserve input order.

func ParseRules

func ParseRules(r io.Reader, opts ParseOptions) ([]Rule, error)

ParseRules parses gitignore-like rules from reader according to opts.

Semantics (defaults, i.e. opts == ParseOptions{}):

  • blank lines are ignored
  • lines starting with the comment prefix ("#" by default) are ignored
  • lines starting with the negation prefix ("!" by default) create an include rule and have the prefix stripped; other lines create an exclude rule
  • "\" + comment prefix and "\" + negation prefix escape a literal leading comment/negation token

See ParseOptions for reconfiguring prefixes and actions.

func ParseRulesString

func ParseRulesString(src string, opts ParseOptions) ([]Rule, error)

ParseRulesString parses rules from string input according to opts.

Jump to

Keyboard shortcuts

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