github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/filter/rules.go (about)

     1  package filter
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/rclone/rclone/fs"
    11  )
    12  
    13  // RulesOpt is configuration for a rule set
    14  type RulesOpt struct {
    15  	FilterRule  []string
    16  	FilterFrom  []string
    17  	ExcludeRule []string
    18  	ExcludeFrom []string
    19  	IncludeRule []string
    20  	IncludeFrom []string
    21  }
    22  
    23  // rule is one filter rule
    24  type rule struct {
    25  	Include bool
    26  	Regexp  *regexp.Regexp
    27  }
    28  
    29  // Match returns true if rule matches path
    30  func (r *rule) Match(path string) bool {
    31  	return r.Regexp.MatchString(path)
    32  }
    33  
    34  // String the rule
    35  func (r *rule) String() string {
    36  	c := "-"
    37  	if r.Include {
    38  		c = "+"
    39  	}
    40  	return fmt.Sprintf("%s %s", c, r.Regexp.String())
    41  }
    42  
    43  // rules is a slice of rules
    44  type rules struct {
    45  	rules    []rule
    46  	existing map[string]struct{}
    47  }
    48  
    49  type addFn func(Include bool, glob string) error
    50  
    51  // add adds a rule if it doesn't exist already
    52  func (rs *rules) add(Include bool, re *regexp.Regexp) {
    53  	if rs.existing == nil {
    54  		rs.existing = make(map[string]struct{})
    55  	}
    56  	newRule := rule{
    57  		Include: Include,
    58  		Regexp:  re,
    59  	}
    60  	newRuleString := newRule.String()
    61  	if _, ok := rs.existing[newRuleString]; ok {
    62  		return // rule already exists
    63  	}
    64  	rs.rules = append(rs.rules, newRule)
    65  	rs.existing[newRuleString] = struct{}{}
    66  }
    67  
    68  // Add adds a filter rule with include or exclude status indicated
    69  func (rs *rules) Add(Include bool, glob string) error {
    70  	re, err := GlobToRegexp(glob, false /* f.Opt.IgnoreCase */)
    71  	if err != nil {
    72  		return err
    73  	}
    74  	rs.add(Include, re)
    75  	return nil
    76  }
    77  
    78  type clearFn func()
    79  
    80  // clear clears all the rules
    81  func (rs *rules) clear() {
    82  	rs.rules = nil
    83  	rs.existing = nil
    84  }
    85  
    86  // len returns the number of rules
    87  func (rs *rules) len() int {
    88  	return len(rs.rules)
    89  }
    90  
    91  // include returns whether this remote passes the filter rules.
    92  func (rs *rules) include(remote string) bool {
    93  	for _, rule := range rs.rules {
    94  		if rule.Match(remote) {
    95  			return rule.Include
    96  		}
    97  	}
    98  	return true
    99  }
   100  
   101  // include returns whether this collection of strings remote passes
   102  // the filter rules.
   103  //
   104  // the first rule is evaluated on all the remotes and if it matches
   105  // then the result is returned. If not the next rule is tested and so
   106  // on.
   107  func (rs *rules) includeMany(remotes []string) bool {
   108  	for _, rule := range rs.rules {
   109  		for _, remote := range remotes {
   110  			if rule.Match(remote) {
   111  				return rule.Include
   112  			}
   113  		}
   114  	}
   115  	return true
   116  }
   117  
   118  // forEachLine calls fn on every line in the file pointed to by path
   119  //
   120  // It ignores empty lines and lines starting with '#' or ';' if raw is false
   121  func forEachLine(path string, raw bool, fn func(string) error) (err error) {
   122  	var scanner *bufio.Scanner
   123  	if path == "-" {
   124  		scanner = bufio.NewScanner(os.Stdin)
   125  	} else {
   126  		in, err := os.Open(path)
   127  		if err != nil {
   128  			return err
   129  		}
   130  		scanner = bufio.NewScanner(in)
   131  		defer fs.CheckClose(in, &err)
   132  	}
   133  	for scanner.Scan() {
   134  		line := scanner.Text()
   135  		if !raw {
   136  			line = strings.TrimSpace(line)
   137  			if len(line) == 0 || line[0] == '#' || line[0] == ';' {
   138  				continue
   139  			}
   140  		}
   141  		err := fn(line)
   142  		if err != nil {
   143  			return err
   144  		}
   145  	}
   146  	return scanner.Err()
   147  }
   148  
   149  // AddRule adds a filter rule with include/exclude indicated by the prefix
   150  //
   151  // These are
   152  //
   153  //	# Comment
   154  //	+ glob
   155  //	- glob
   156  //	!
   157  //
   158  // '+' includes the glob, '-' excludes it and '!' resets the filter list
   159  //
   160  // Line comments may be introduced with '#' or ';'
   161  func addRule(rule string, add addFn, clear clearFn) error {
   162  	switch {
   163  	case rule == "!":
   164  		clear()
   165  		return nil
   166  	case strings.HasPrefix(rule, "- "):
   167  		return add(false, rule[2:])
   168  	case strings.HasPrefix(rule, "+ "):
   169  		return add(true, rule[2:])
   170  	}
   171  	return fmt.Errorf("malformed rule %q", rule)
   172  }
   173  
   174  // AddRule adds a filter rule with include/exclude indicated by the prefix
   175  //
   176  // These are
   177  //
   178  //	# Comment
   179  //	+ glob
   180  //	- glob
   181  //	!
   182  //
   183  // '+' includes the glob, '-' excludes it and '!' resets the filter list
   184  //
   185  // Line comments may be introduced with '#' or ';'
   186  func (rs *rules) AddRule(rule string) error {
   187  	return addRule(rule, rs.Add, rs.clear)
   188  }
   189  
   190  // Parse the rules passed in and add them to the function
   191  func parseRules(opt *RulesOpt, add addFn, clear clearFn) (err error) {
   192  	addImplicitExclude := false
   193  	foundExcludeRule := false
   194  
   195  	for _, rule := range opt.IncludeRule {
   196  		err = add(true, rule)
   197  		if err != nil {
   198  			return err
   199  		}
   200  		addImplicitExclude = true
   201  	}
   202  	for _, rule := range opt.IncludeFrom {
   203  		err := forEachLine(rule, false, func(line string) error {
   204  			return add(true, line)
   205  		})
   206  		if err != nil {
   207  			return err
   208  		}
   209  		addImplicitExclude = true
   210  	}
   211  	for _, rule := range opt.ExcludeRule {
   212  		err = add(false, rule)
   213  		if err != nil {
   214  			return err
   215  		}
   216  		foundExcludeRule = true
   217  	}
   218  	for _, rule := range opt.ExcludeFrom {
   219  		err := forEachLine(rule, false, func(line string) error {
   220  			return add(false, line)
   221  		})
   222  		if err != nil {
   223  			return err
   224  		}
   225  		foundExcludeRule = true
   226  	}
   227  
   228  	if addImplicitExclude && foundExcludeRule {
   229  		fs.Errorf(nil, "Using --filter is recommended instead of both --include and --exclude as the order they are parsed in is indeterminate")
   230  	}
   231  
   232  	for _, rule := range opt.FilterRule {
   233  		err = addRule(rule, add, clear)
   234  		if err != nil {
   235  			return err
   236  		}
   237  	}
   238  	for _, rule := range opt.FilterFrom {
   239  		err := forEachLine(rule, false, func(rule string) error {
   240  			return addRule(rule, add, clear)
   241  		})
   242  		if err != nil {
   243  			return err
   244  		}
   245  	}
   246  
   247  	if addImplicitExclude {
   248  		err = add(false, "/**")
   249  		if err != nil {
   250  			return err
   251  		}
   252  	}
   253  
   254  	return nil
   255  }