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 }