"Fossies" - the Fresh Open Source Software Archive 
Member "sift-0.9.0/options.go" (22 Oct 2016, 33911 Bytes) of package /linux/privat/sift-0.9.0.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Go source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
See also the latest
Fossies "Diffs" side-by-side code changes report for "options.go":
0.8.0_vs_0.9.0.
1 // sift
2 // Copyright (C) 2014-2016 Sven Taute
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, version 3 of the License.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16 package main
17
18 import (
19 "encoding/json"
20 "errors"
21 "fmt"
22 "io/ioutil"
23 "net"
24 "os"
25 "os/user"
26 "path/filepath"
27 "regexp"
28 "runtime"
29 "sort"
30 "strconv"
31 "strings"
32
33 "golang.org/x/crypto/ssh/terminal"
34 )
35
36 type Options struct {
37 BinarySkip bool `long:"binary-skip" description:"skip files that seem to be binary"`
38 BinaryAsText bool `short:"a" long:"binary-text" description:"process files that seem to be binary as text"`
39 Blocksize string `long:"blocksize" description:"blocksize in bytes (with optional suffix K|M)"`
40 Color string
41 ColorFunc func() `long:"color" description:"enable colored output (default: auto)" json:"-"`
42 NoColorFunc func() `long:"no-color" description:"disable colored output" json:"-"`
43 ConfigFile string `long:"conf" description:"load config file FILE" value-name:"FILE" json:"-"`
44 Context int `short:"C" long:"context" description:"show NUM context lines" value-name:"NUM" json:"-"`
45 ContextAfter int `short:"A" long:"context-after" description:"show NUM context lines after match" value-name:"NUM" json:"-"`
46 ContextBefore int `short:"B" long:"context-before" description:"show NUM context lines before match" value-name:"NUM" json:"-"`
47 Cores int `short:"j" long:"cores" description:"limit used CPU Cores (default: 0 = all)" default-mask:"-"`
48 Count bool `short:"c" long:"count" description:"print count of matches per file" json:"-"`
49 IncludeDirs []string `long:"dirs" description:"recurse only into directories whose name matches GLOB" value-name:"GLOB" default-mask:"-"`
50 ErrShowLineLength bool `long:"err-show-line-length" description:"show all line length errors"`
51 ErrSkipLineLength bool `long:"err-skip-line-length" description:"skip line length errors"`
52 ExcludeDirs []string `long:"exclude-dirs" description:"do not recurse into directories whose name matches GLOB" value-name:"GLOB" default-mask:"-"`
53 IncludeExtensions string `short:"x" long:"ext" description:"limit search to specific file extensions (comma-separated)" default-mask:"-"`
54 ExcludeExtensions string `short:"X" long:"exclude-ext" description:"exclude specific file extensions (comma-separated)" default-mask:"-"`
55 IncludeFiles []string `long:"files" description:"search only files whose name matches GLOB" value-name:"GLOB" default-mask:"-"`
56 ExcludeFiles []string `long:"exclude-files" description:"do not select files whose name matches GLOB while recursing" value-name:"GLOB" default-mask:"-"`
57 IncludePath string `long:"path" description:"search only files whose path matches PATTERN" value-name:"PATTERN" default-mask:"-"`
58 IncludeIPath string `long:"ipath" description:"search only files whose path matches PATTERN (case insensitive)" value-name:"PATTERN" default-mask:"-"`
59 ExcludePath string `long:"exclude-path" description:"do not search files whose path matches PATTERN" value-name:"PATTERN" default-mask:"-"`
60 ExcludeIPath string `long:"exclude-ipath" description:"do not search files whose path matches PATTERN (case insensitive)" value-name:"PATTERN" default-mask:"-"`
61 IncludeTypes string `short:"t" long:"type" description:"limit search to specific file types (comma-separated, see --list-types)" default-mask:"-"`
62 ExcludeTypes string `short:"T" long:"no-type" description:"exclude specific file types (comma-separated, see --list-types)" default-mask:"-"`
63 AddCustomTypes []string `long:"add-type" description:"add custom type (see --list-types for format)" default-mask:"-" json:"-"`
64 DelCustomTypes []string `long:"del-type" description:"remove custom type" default-mask:"-" json:"-"`
65 CustomTypes map[string]string
66 FieldSeparator string `long:"field-sep" description:"column separator (default: \":\")" default-mask:"-"`
67 FilesWithMatches bool `short:"l" long:"files-with-matches" description:"list files containing matches"`
68 FilesWithoutMatch bool `short:"L" long:"files-without-match" description:"list files containing no match"`
69 FollowSymlinks bool `long:"follow" description:"follow symlinks"`
70 Git bool `long:"git" description:"respect .gitignore files and skip .git directories"`
71 GroupByFile bool `long:"group" description:"group output by file (default: off)"`
72 NoGroupByFile func() `long:"no-group" description:"do not group output by file" json:"-"`
73 IgnoreCase bool `short:"i" long:"ignore-case" description:"case insensitive (default: off)"`
74 NoIgnoreCase func() `short:"I" long:"no-ignore-case" description:"disable case insensitive" json:"-"`
75 SmartCase bool `short:"s" long:"smart-case" description:"case insensitive unless pattern contains uppercase characters (default: off)"`
76 NoSmartCase func() `short:"S" long:"no-smart-case" description:"disable smart case" json:"-"`
77 NoConfig bool `long:"no-conf" description:"do not load config files" json:"-"`
78 InvertMatch bool `short:"v" long:"invert-match" description:"select non-matching lines" json:"-"`
79 Limit int64 `long:"limit" description:"only show first NUM matches per file" value-name:"NUM" default-mask:"-"`
80 Literal bool `short:"Q" long:"literal" description:"treat pattern as literal, quote meta characters"`
81 Multiline bool `short:"m" long:"multiline" description:"multiline parsing (default: off)"`
82 NoMultiline func() `short:"M" long:"no-multiline" description:"disable multiline parsing" json:"-"`
83 OnlyMatching bool `long:"only-matching" description:"only show the matching part of a line" json:"-"`
84 Output string `short:"o" long:"output" description:"write output to the specified file or network connection" value-name:"FILE|tcp://HOST:PORT" json:"-"`
85 OutputLimit int `long:"output-limit" description:"limit output length per found match" default-mask:"-"`
86 OutputSeparator string `long:"output-sep" description:"output separator (default: \"\\n\")" default-mask:"-" json:"-"`
87 OutputUnixPath bool `long:"output-unixpath" description:"output file paths in unix format ('/' as path separator)"`
88 Patterns []string `short:"e" long:"regexp" description:"add pattern PATTERN to the search" value-name:"PATTERN" default-mask:"-" json:"-"`
89 PatternFile string `short:"f" long:"regexp-file" description:"search for patterns contained in FILE (one per line)" value-name:"FILE" default-mask:"-" json:"-"`
90 PrintConfig bool `long:"print-config" description:"print config for loaded configs + given command line arguments" json:"-"`
91 Quiet bool `short:"q" long:"quiet" description:"suppress output, exit with return code zero if any match is found" json:"-"`
92 Recursive bool `short:"r" long:"recursive" description:"recurse into directories (default: on)"`
93 NoRecursive func() `short:"R" long:"no-recursive" description:"do not recurse into directories" json:"-"`
94 Replace string `long:"replace" description:"replace numbered or named (?P<name>pattern) capture groups. Use ${1}, ${2}, $name, ... for captured submatches" json:"-"`
95 ShowFilename string
96 ShowFilenameFunc func() `long:"filename" description:"enforce printing the filename before results (default: auto)" json:"-"`
97 NoShowFilenameFunc func() `long:"no-filename" description:"disable printing the filename before results" json:"-"`
98 ShowLineNumbers bool `short:"n" long:"line-number" description:"show line numbers (default: off)"`
99 NoShowLineNumbers func() `short:"N" long:"no-line-number" description:"do not show line numbers" json:"-"`
100 ShowColumnNumbers bool `long:"column" description:"show column numbers"`
101 NoShowColumnNumbers func() `long:"no-column" description:"do not show column numbers" json:"-"`
102 ShowByteOffset bool `long:"byte-offset" description:"show the byte offset before each output line"`
103 NoShowByteOffset func() `long:"no-byte-offset" description:"do not show the byte offset before each output line" json:"-"`
104 Stats bool `long:"stats" description:"show statistics"`
105 TargetsOnly bool `long:"targets" description:"only list selected files, do not search"`
106 ListTypes bool `long:"list-types" description:"list available file types" json:"-" default-mask:"-"`
107 Version func() `short:"V" long:"version" description:"show version and license information" json:"-"`
108 WordRegexp bool `short:"w" long:"word-regexp" description:"only match on ASCII word boundaries"`
109 WriteConfig bool `long:"write-config" description:"save config for loaded configs + given command line arguments" json:"-"`
110 Zip bool `short:"z" long:"zip" description:"search content of compressed .gz files (default: off)"`
111 NoZip func() `short:"Z" long:"no-zip" description:"do not search content of compressed .gz files" json:"-"`
112
113 FileConditions struct {
114 FileMatches []string `long:"file-matches" description:"only show matches if file also matches PATTERN" value-name:"PATTERN"`
115 LineMatches []string `long:"line-matches" description:"only show matches if line NUM matches PATTERN" value-name:"NUM:PATTERN"`
116 RangeMatches []string `long:"range-matches" description:"only show matches if lines X-Y match PATTERN" value-name:"X:Y:PATTERN"`
117 NotFileMatches []string `long:"not-file-matches" description:"only show matches if file does not match PATTERN" value-name:"PATTERN"`
118 NotLineMatches []string `long:"not-line-matches" description:"only show matches if line NUM does not match PATTERN" value-name:"NUM:PATTERN"`
119 NotRangeMatches []string `long:"not-range-matches" description:"only show matches if lines X-Y do not match PATTERN" value-name:"X:Y:PATTERN"`
120 } `group:"File Condition options" json:"-"`
121
122 MatchConditions struct {
123 Preceded []string `long:"preceded-by" description:"only show matches preceded by PATTERN" value-name:"PATTERN"`
124 Followed []string `long:"followed-by" description:"only show matches followed by PATTERN" value-name:"PATTERN"`
125 Surrounded []string `long:"surrounded-by" description:"only show matches surrounded by PATTERN" value-name:"PATTERN"`
126 PrecededWithin []string `long:"preceded-within" description:"only show matches preceded by PATTERN within NUM lines" value-name:"NUM:PATTERN"`
127 FollowedWithin []string `long:"followed-within" description:"only show matches followed by PATTERN within NUM lines" value-name:"NUM:PATTERN"`
128 SurroundedWithin []string `long:"surrounded-within" description:"only show matches surrounded by PATTERN within NUM lines" value-name:"NUM:PATTERN"`
129 NotPreceded []string `long:"not-preceded-by" description:"only show matches not preceded by PATTERN" value-name:"PATTERN"`
130 NotFollowed []string `long:"not-followed-by" description:"only show matches not followed by PATTERN" value-name:"PATTERN"`
131 NotSurrounded []string `long:"not-surrounded-by" description:"only show matches not surrounded by PATTERN" value-name:"PATTERN"`
132 NotPrecededWithin []string `long:"not-preceded-within" description:"only show matches not preceded by PATTERN within NUM lines" value-name:"NUM:PATTERN"`
133 NotFollowedWithin []string `long:"not-followed-within" description:"only show matches not followed by PATTERN within NUM lines" value-name:"NUM:PATTERN"`
134 NotSurroundedWithin []string `long:"not-surrounded-within" description:"only show matches not surrounded by PATTERN within NUM lines" value-name:"NUM:PATTERN"`
135 } `group:"Match Condition options" json:"-"`
136 }
137
138 func getHomeDir() string {
139 var home string
140 if runtime.GOOS == "windows" {
141 home = os.Getenv("USERPROFILE")
142 } else {
143 home = os.Getenv("HOME")
144 }
145 if home == "" {
146 if u, err := user.Current(); err == nil {
147 home = u.HomeDir
148 }
149 }
150 return home
151 }
152
153 // findLocalConfig returns the path to the local config file.
154 // It searches the current directory and all parent directories for a config file.
155 // If no config file is found, findLocalConfig returns an empty string.
156 func findLocalConfig() string {
157 curdir, err := os.Getwd()
158 if err != nil {
159 curdir = "."
160 }
161 path, err := filepath.Abs(curdir)
162 if err != nil || path == "" {
163 return ""
164 }
165 lp := ""
166 for path != lp {
167 confpath := filepath.Join(path, SiftConfigFile)
168 if _, err := os.Stat(confpath); err == nil {
169 return confpath
170 }
171 lp = path
172 path = filepath.Dir(path)
173 }
174 return ""
175 }
176
177 // func listTypes list the available types (built-in and custom) and exits.
178 func listTypes() {
179 fmt.Println("The following list shows all file types supported.")
180 fmt.Println("Use --type/--no-type to include/exclude file types.")
181 fmt.Println("")
182 var types []string
183 for t := range global.fileTypesMap {
184 types = append(types, t)
185 }
186 sort.Strings(types)
187 for _, e := range types {
188 t := global.fileTypesMap[e]
189 var shebang string
190 if t.ShebangRegex != nil {
191 shebang = fmt.Sprintf("or first line matches /%s/", t.ShebangRegex)
192 }
193 fmt.Printf("%-15s:%s %s\n", e, strings.Join(t.Patterns, " "), shebang)
194 }
195 fmt.Println("")
196 fmt.Println(`Custom types can be added with --add-type.`)
197 fmt.Println(`Example matching *.rb, *.erb, Rakefile and all files whose first line matches the regular expression /\bruby\b/:`)
198 fmt.Println(`sift --add-type 'ruby=*.rb,*.erb,Rakefile;\bruby\b'`)
199 fmt.Println(`Write the definition to the config file:`)
200 fmt.Println(`sift --add-type 'ruby=*.rb,*.erb,Rakefile;\bruby\b' --write-config`)
201 fmt.Println(`Remove the definition from the config file:`)
202 fmt.Println(`sift --del-type ruby --write-config`)
203 fmt.Println("")
204 os.Exit(0)
205 }
206
207 // LoadDefaults sets default options.
208 func (o *Options) LoadDefaults() {
209 o.Cores = runtime.NumCPU()
210 o.OutputSeparator = ""
211 o.FieldSeparator = ":"
212 o.ShowFilename = "auto"
213 o.Color = "auto"
214 o.Recursive = true
215 o.CustomTypes = make(map[string]string)
216
217 o.ColorFunc = func() {
218 o.Color = "on"
219 }
220 o.NoColorFunc = func() {
221 o.Color = "off"
222 }
223 o.NoIgnoreCase = func() {
224 o.IgnoreCase = false
225 }
226 o.NoSmartCase = func() {
227 o.SmartCase = false
228 }
229 o.NoGroupByFile = func() {
230 o.GroupByFile = false
231 }
232 o.NoMultiline = func() {
233 o.Multiline = false
234 }
235 o.NoRecursive = func() {
236 o.Recursive = false
237 }
238 o.ShowFilenameFunc = func() {
239 o.ShowFilename = "on"
240 }
241 o.NoShowFilenameFunc = func() {
242 o.ShowFilename = "off"
243 }
244 o.NoShowLineNumbers = func() {
245 o.ShowLineNumbers = false
246 }
247 o.NoShowColumnNumbers = func() {
248 o.ShowColumnNumbers = false
249 }
250 o.NoShowByteOffset = func() {
251 o.ShowByteOffset = false
252 }
253 o.NoZip = func() {
254 o.Zip = false
255 }
256 o.Version = func() {
257 fmt.Printf("sift %s (%s/%s)\n", SiftVersion, runtime.GOOS, runtime.GOARCH)
258 fmt.Println("Copyright (C) 2014-2016 Sven Taute")
259 fmt.Println("")
260 fmt.Println("This program is free software: you can redistribute it and/or modify")
261 fmt.Println("it under the terms of the GNU General Public License as published by")
262 fmt.Println("the Free Software Foundation, version 3 of the License.")
263 fmt.Println("")
264 fmt.Println("This program is distributed in the hope that it will be useful,")
265 fmt.Println("but WITHOUT ANY WARRANTY; without even the implied warranty of")
266 fmt.Println("MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the")
267 fmt.Println("GNU General Public License for more details.")
268 fmt.Println("")
269 fmt.Println("You should have received a copy of the GNU General Public License")
270 fmt.Println("along with this program. If not, see <http://www.gnu.org/licenses/>.")
271 os.Exit(0)
272 }
273 }
274
275 // loadConfigFile loads options from the given config file.
276 func (o *Options) loadConfigFile(configFilePath string, label string) {
277 configFile, err := ioutil.ReadFile(configFilePath)
278 if err == nil && len(configFile) > 0 {
279 if err := json.Unmarshal(configFile, &o); err != nil {
280 errorLogger.Printf("cannot parse %s '%s': %s\n", label, configFilePath, err)
281 }
282 }
283 if err != nil {
284 errorLogger.Printf("cannot open %s '%s': %s\n", label, configFilePath, err)
285 }
286 }
287
288 // LoadConfigs tries to load options from sift config files.
289 // if noConf is true, only a config file set via option --conf will be parsed.
290 func (o *Options) LoadConfigs(noConf bool, configFileArg string) {
291 if !noConf {
292 // load config from global sift config if file exists
293 if homedir := getHomeDir(); homedir != "" {
294 configFilePath := filepath.Join(homedir, SiftConfigFile)
295 if _, err := os.Stat(configFilePath); err == nil {
296 o.loadConfigFile(configFilePath, "global config")
297 }
298 }
299
300 // load config from local sift config if file exists
301 if configFilePath := findLocalConfig(); configFilePath != "" {
302 if _, err := os.Stat(configFilePath); err == nil {
303 o.loadConfigFile(configFilePath, "local config")
304 }
305 }
306 }
307
308 // load config from config option
309 if configFileArg != "" {
310 o.loadConfigFile(configFileArg, "config")
311 }
312 }
313
314 // Apply processes user provided options
315 func (o *Options) Apply(patterns []string, targets []string) error {
316 if err := o.processTypes(); err != nil {
317 return err
318 }
319
320 if err := o.checkFormats(); err != nil {
321 return err
322 }
323
324 if err := o.processConditions(); err != nil {
325 return err
326 }
327
328 if err := o.checkCompatibility(patterns, targets); err != nil {
329 return err
330 }
331
332 // handle print-config and write-config before auto detection to prevent
333 // auto detected values from being written to the config file
334 if err := o.processConfigOptions(); err != nil {
335 return err
336 }
337
338 o.performAutoDetections(patterns, targets)
339
340 if o.Quiet {
341 global.outputFile = ioutil.Discard
342 }
343
344 if o.OnlyMatching {
345 o.Replace = `$0`
346 }
347
348 for i := range patterns {
349 patterns[i] = o.preparePattern(patterns[i])
350 }
351
352 runtime.GOMAXPROCS(o.Cores)
353 return nil
354 }
355
356 // processTypes processes custom types defined on the command line
357 // or in the config file.
358 func (o *Options) processTypes() error {
359 for _, e := range o.DelCustomTypes {
360 if _, ok := o.CustomTypes[e]; !ok {
361 return fmt.Errorf("No custom type definition for '%s' found", e)
362 }
363 delete(o.CustomTypes, e)
364 }
365
366 for _, e := range o.AddCustomTypes {
367 s := strings.SplitN(e, "=", 2)
368 if len(s) != 2 {
369 return fmt.Errorf("wrong format for type definition '%s'", e)
370 }
371 o.CustomTypes[s[0]] = s[1]
372 }
373
374 // parse type definition, e.g. '*.pl,*.pm;\bperl\b'
375 for name, e := range o.CustomTypes {
376 var ft FileType
377 s := strings.SplitN(e, ";", 2)
378 if len(s) == 2 && s[1] != "" {
379 re, err := regexp.Compile(s[1])
380 if err != nil {
381 return fmt.Errorf("cannot parse regular expression '%s' for custom type '%s': %s", s[1], name, err)
382 }
383 ft.ShebangRegex = re
384 }
385 patterns := strings.Split(s[0], ",")
386 ft.Patterns = patterns
387 global.fileTypesMap[name] = ft
388 }
389
390 if o.ListTypes {
391 listTypes()
392 }
393
394 return nil
395 }
396
397 // checkFormats checks options for illegal formats
398 func (o *Options) checkFormats() error {
399 if o.ExcludePath != "" {
400 var err error
401 global.excludeFilepathRegex, err = regexp.Compile(o.ExcludePath)
402 if err != nil {
403 return fmt.Errorf("cannot parse exclude filepath pattern '%s': %s\n", o.ExcludePath, err)
404 }
405 }
406 if o.ExcludeIPath != "" {
407 var err error
408 global.excludeFilepathRegex, err = regexp.Compile("(?i)" + o.ExcludeIPath)
409 if err != nil {
410 return fmt.Errorf("cannot parse exclude filepath pattern '%s': %s\n", o.ExcludeIPath, err)
411 }
412 }
413 if o.IncludePath != "" {
414 var err error
415 global.includeFilepathRegex, err = regexp.Compile(o.IncludePath)
416 if err != nil {
417 return fmt.Errorf("cannot parse filepath pattern '%s': %s\n", o.IncludePath, err)
418 }
419 }
420 if o.IncludeIPath != "" {
421 var err error
422 global.includeFilepathRegex, err = regexp.Compile("(?i)" + o.IncludeIPath)
423 if err != nil {
424 return fmt.Errorf("cannot parse filepath pattern '%s': %s\n", o.IncludeIPath, err)
425 }
426 }
427
428 if len(o.IncludeTypes) > 0 {
429 for _, t := range strings.Split(o.IncludeTypes, ",") {
430 if _, ok := global.fileTypesMap[t]; !ok {
431 return fmt.Errorf("file type '%s' is not specified. See --list-types for a list of available file types", t)
432 }
433 }
434 }
435 if len(o.ExcludeTypes) > 0 {
436 for _, t := range strings.Split(o.ExcludeTypes, ",") {
437 if _, ok := global.fileTypesMap[t]; !ok {
438 return fmt.Errorf("file type '%s' is not specified. See --list-types for a list of available file types", t)
439 }
440 }
441 }
442
443 if options.Cores < 0 {
444 return fmt.Errorf("the number of cores must be >= 1 (or 0 for 'all')")
445 }
446
447 if options.Blocksize != "" {
448 re := regexp.MustCompile(`^\d+[kKmM]?$`)
449 if !re.MatchString(options.Blocksize) {
450 return fmt.Errorf("cannot parse blocksize %q", options.Blocksize)
451 }
452 var blocksize int
453 switch options.Blocksize[len(options.Blocksize)-1:] {
454 case "k", "K":
455 blocksize, _ = strconv.Atoi(options.Blocksize[0 : len(options.Blocksize)-1])
456 InputBlockSize = blocksize * 1024
457 case "m", "M":
458 blocksize, _ = strconv.Atoi(options.Blocksize[0 : len(options.Blocksize)-1])
459 InputBlockSize = blocksize * 1024 * 1024
460 default:
461 blocksize, _ := strconv.Atoi(options.Blocksize)
462 InputBlockSize = blocksize
463 }
464 if InputBlockSize < 256*1024 {
465 return fmt.Errorf("blocksize must be >= 256k")
466 }
467 }
468
469 if o.OutputSeparator == "" {
470 o.OutputSeparator = "\n"
471 } else {
472 sep, err := strconv.Unquote("\"" + o.OutputSeparator + "\"")
473 if err != nil {
474 return fmt.Errorf("cannot parse output separator '%s': %s\n", o.OutputSeparator, err)
475 }
476 o.OutputSeparator = sep
477 }
478
479 if o.Output != "" {
480 if global.netTcpRegex.MatchString(o.Output) {
481 netParams := global.netTcpRegex.FindStringSubmatch(o.Output)
482 proto := netParams[1]
483 addr := netParams[2]
484 conn, err := net.Dial(proto, addr)
485 if err != nil {
486 return fmt.Errorf("could not connect to '%s'", o.Output)
487 }
488 global.outputFile = conn
489 } else {
490 writer, err := os.Create(o.Output)
491 if err != nil {
492 return fmt.Errorf("cannot open output file '%s' for writing", o.Output)
493 }
494 global.outputFile = writer
495 }
496 }
497
498 return nil
499 }
500
501 // preparePattern adjusts a pattern to respect the ignore-case, literal and multiline options
502 func (o *Options) preparePattern(pattern string) string {
503 if o.Literal {
504 pattern = regexp.QuoteMeta(pattern)
505 }
506 if o.IgnoreCase {
507 pattern = strings.ToLower(pattern)
508 }
509 if o.WordRegexp {
510 pattern = `\b` + pattern + `\b`
511 }
512 pattern = "(?m)" + pattern
513 if o.Multiline {
514 pattern = "(?s)" + pattern
515 }
516 return pattern
517 }
518
519 // processConditions checks conditions and puts them into global.conditions
520 func (o *Options) processConditions() error {
521 global.conditions = []Condition{}
522 conditionDirections := []ConditionType{ConditionPreceded, ConditionFollowed, ConditionSurrounded}
523
524 // parse preceded/followed/surrounded conditions without distance limit
525 conditionArgs := [][]string{o.MatchConditions.Preceded, o.MatchConditions.Followed, o.MatchConditions.Surrounded,
526 o.MatchConditions.NotPreceded, o.MatchConditions.NotFollowed, o.MatchConditions.NotSurrounded}
527 for i := range conditionArgs {
528 for _, pattern := range conditionArgs[i] {
529 regex, err := regexp.Compile(o.preparePattern(pattern))
530 if err != nil {
531 return fmt.Errorf("cannot parse condition pattern '%s': %s\n", pattern, err)
532 }
533 global.conditions = append(global.conditions, Condition{regex: regex, conditionType: conditionDirections[i%3], within: -1, negated: i >= 3})
534 }
535 }
536
537 // parse preceded/followed/surrounded conditions with distance limit
538 conditionArgs = [][]string{o.MatchConditions.PrecededWithin, o.MatchConditions.FollowedWithin, o.MatchConditions.SurroundedWithin,
539 o.MatchConditions.NotPrecededWithin, o.MatchConditions.NotFollowedWithin, o.MatchConditions.NotSurroundedWithin}
540 for i := range conditionArgs {
541 for _, arg := range conditionArgs[i] {
542 s := strings.SplitN(arg, ":", 2)
543 if len(s) != 2 {
544 return fmt.Errorf("wrong format for condition option '%s'\n", arg)
545 }
546 within, err := strconv.Atoi(s[0])
547 if err != nil {
548 return fmt.Errorf("cannot parse condition option '%s': '%s' is not a number\n", arg, s[0])
549 }
550 if within < 0 {
551 return fmt.Errorf("distance value must be >= 0\n")
552 }
553 regex, err := regexp.Compile(o.preparePattern(s[1]))
554 if err != nil {
555 return fmt.Errorf("cannot parse condition pattern '%s': %s", arg, err)
556 }
557 global.conditions = append(global.conditions, Condition{regex: regex, conditionType: conditionDirections[i%3], within: int64(within), negated: i >= 3})
558 }
559 }
560
561 // parse match conditions
562 conditionArgs = [][]string{o.FileConditions.FileMatches, o.FileConditions.NotFileMatches}
563 for i := range conditionArgs {
564 for _, pattern := range conditionArgs[i] {
565 regex, err := regexp.Compile(o.preparePattern(pattern))
566 if err != nil {
567 return fmt.Errorf("cannot parse condition pattern '%s': %s\n", pattern, err)
568 }
569 global.conditions = append(global.conditions, Condition{regex: regex, conditionType: ConditionFileMatches, negated: i == 1})
570 }
571 }
572
573 // parse line match conditions
574 conditionArgs = [][]string{o.FileConditions.LineMatches, o.FileConditions.NotLineMatches}
575 for i := range conditionArgs {
576 for _, arg := range conditionArgs[i] {
577 s := strings.SplitN(arg, ":", 2)
578 if len(s) != 2 {
579 return fmt.Errorf("wrong format for condition option '%s'\n", arg)
580 }
581 lineno, err := strconv.Atoi(s[0])
582 if err != nil {
583 return fmt.Errorf("cannot parse condition option '%s': '%s' is not a number\n", arg, s[0])
584 }
585 if lineno < 1 {
586 return fmt.Errorf("line number value must be > 0\n")
587 }
588 regex, err := regexp.Compile(o.preparePattern(s[1]))
589 if err != nil {
590 return fmt.Errorf("cannot parse condition pattern '%s': %s\n", s[1], err)
591 }
592 global.conditions = append(global.conditions, Condition{regex: regex, conditionType: ConditionLineMatches, lineRangeStart: int64(lineno), negated: i == 1})
593 }
594 }
595
596 // parse line range match conditions
597 conditionArgs = [][]string{o.FileConditions.RangeMatches, o.FileConditions.NotRangeMatches}
598 for i := range conditionArgs {
599 for _, arg := range conditionArgs[i] {
600 s := strings.SplitN(arg, ":", 3)
601 if len(s) != 3 {
602 return fmt.Errorf("wrong format for condition option '%s'\n", arg)
603 }
604 lineStart, err := strconv.Atoi(s[0])
605 if err != nil {
606 return fmt.Errorf("cannot parse condition option '%s': '%s' is not a number\n", arg, s[0])
607 }
608 lineEnd, err := strconv.Atoi(s[1])
609 if err != nil {
610 return fmt.Errorf("cannot parse condition option '%s': '%s' is not a number\n", arg, s[1])
611 }
612 if lineStart < 1 || lineEnd < 1 {
613 return fmt.Errorf("line number value must be > 0\n")
614 }
615 regex, err := regexp.Compile(o.preparePattern(s[2]))
616 if err != nil {
617 return fmt.Errorf("cannot parse condition pattern '%s': %s\n", s[2], err)
618 }
619 global.conditions = append(global.conditions, Condition{regex: regex, conditionType: ConditionRangeMatches, lineRangeStart: int64(lineStart), lineRangeEnd: int64(lineEnd), negated: i == 1})
620 }
621 }
622
623 return nil
624 }
625
626 // checkCompatibility checks options for incompatible combinations
627 func (o *Options) checkCompatibility(patterns []string, targets []string) error {
628 stdinTargetFound := false
629 netTargetFound := false
630 for _, target := range targets {
631 switch {
632 case target == "-":
633 stdinTargetFound = true
634 case global.netTcpRegex.MatchString(target):
635 netTargetFound = true
636 }
637 }
638 if o.Context > 0 {
639 o.ContextBefore = o.Context
640 o.ContextAfter = o.Context
641 }
642
643 if o.InvertMatch && o.Multiline {
644 return errors.New("options 'multiline' and 'invert' cannot be used together")
645 }
646 if netTargetFound && o.InvertMatch {
647 return errors.New("option 'invert' is not supported for network targets")
648 }
649 if o.OutputLimit < 0 {
650 return errors.New("value for option 'output-limit' must be >= 0 (0 = no limit)")
651 }
652
653 if o.OutputSeparator != "\n" && (o.ContextBefore > 0 || o.ContextAfter > 0) {
654 return errors.New("context options are not supported when combined with a non-standard 'output-separator'")
655 }
656
657 if (stdinTargetFound || netTargetFound) && (o.ContextBefore > 0 || o.ContextAfter > 0) {
658 return errors.New("context options are not supported when reading from STDIN or network")
659 }
660
661 if (stdinTargetFound || netTargetFound) && o.TargetsOnly {
662 return errors.New("targets option not supported when reading from STDIN or network")
663 }
664
665 if (o.ContextBefore != 0 || o.ContextAfter != 0) && (o.Count || o.FilesWithMatches || o.FilesWithoutMatch) {
666 return errors.New("context options cannot be combined with count or list option")
667 }
668
669 if o.FilesWithMatches && o.FilesWithoutMatch {
670 return errors.New("illegal combination of list option")
671 }
672
673 if o.Zip && (o.ContextBefore != 0 || o.ContextAfter != 0) {
674 return errors.New("context options cannot be used with zip search enabled")
675 }
676
677 if o.BinarySkip && o.BinaryAsText {
678 return errors.New("options 'binary-skip' and 'binary-text' cannot be used together")
679 }
680
681 if o.ErrSkipLineLength && o.ErrShowLineLength {
682 return errors.New("options 'err-skip-line-length' and 'err-show-line-length' cannot be used together")
683 }
684
685 if o.OnlyMatching && o.Replace != "" {
686 return errors.New("options 'only-matching' and 'replace' cannot be used together")
687 }
688
689 if o.SmartCase && (len(patterns) > 1 || len(global.conditions) > 0) {
690 return errors.New("the smart case option cannot be used with multiple patterns or conditions")
691 }
692
693 if o.ExcludePath != "" && o.ExcludeIPath != "" {
694 return errors.New("options 'exclude-path' and 'exclude-ipath' cannot be used together")
695 }
696 if o.IncludePath != "" && o.IncludeIPath != "" {
697 return errors.New("options 'path' and 'ipath' cannot be used together")
698 }
699
700 return nil
701 }
702
703 // processConfigOptions processes the options --print-config and --write-config
704 func (o *Options) processConfigOptions() error {
705 if o.PrintConfig {
706 if homedir := getHomeDir(); homedir != "" {
707 globalConfigFilePath := filepath.Join(homedir, SiftConfigFile)
708 fmt.Fprintf(os.Stderr, "Global config file path: %s\n", globalConfigFilePath)
709 } else {
710 errorLogger.Println("could not detect user home directory.")
711 }
712
713 localConfigFilePath := findLocalConfig()
714 if localConfigFilePath != "" {
715 fmt.Fprintf(os.Stderr, "Local config file path: %s\n", localConfigFilePath)
716 } else {
717 fmt.Fprintf(os.Stderr, "No local config file found.\n")
718 }
719
720 conf, err := json.MarshalIndent(o, "", " ")
721 if err != nil {
722 return fmt.Errorf("cannot convert config to JSON: %s", err)
723 }
724 fmt.Println(string(conf))
725 os.Exit(0)
726 }
727
728 if o.WriteConfig {
729 var configFilePath string
730 localConfigFilePath := findLocalConfig()
731 if localConfigFilePath != "" {
732 configFilePath = localConfigFilePath
733 } else {
734 if homedir := getHomeDir(); homedir != "" {
735 configFilePath = filepath.Join(homedir, SiftConfigFile)
736 } else {
737 return errors.New("could not detect user home directory")
738 }
739 }
740 conf, err := json.MarshalIndent(o, "", " ")
741 if err != nil {
742 return fmt.Errorf("cannot convert config to JSON: %s", err)
743 }
744 if err := ioutil.WriteFile(configFilePath, conf, os.ModePerm); err != nil {
745 return fmt.Errorf("cannot write config file: %s", err)
746 }
747 fmt.Printf("Saved config to '%s'.\n", configFilePath)
748 os.Exit(0)
749 }
750
751 return nil
752 }
753
754 // performAutoDetections sets options that are set to "auto"
755 func (o *Options) performAutoDetections(patterns []string, targets []string) {
756 stdinTargetFound := false
757 netTargetFound := false
758 for _, target := range targets {
759 switch {
760 case target == "-":
761 stdinTargetFound = true
762 case global.netTcpRegex.MatchString(target):
763 netTargetFound = true
764 }
765 }
766
767 if len(global.conditions) == 0 {
768 global.streamingAllowed = true
769
770 if len(targets) == 1 {
771 if stdinTargetFound || netTargetFound {
772 global.streamingThreshold = 0
773 o.GroupByFile = false
774 } else {
775 stat, err := os.Stat(targets[0])
776 if err == nil && stat.Mode()&os.ModeType == 0 {
777 global.streamingThreshold = 0
778 }
779 }
780 }
781 }
782
783 if o.ShowFilename == "auto" {
784 if len(targets) == 1 {
785 fileinfo, err := os.Stat(targets[0])
786 if err == nil && fileinfo.IsDir() {
787 o.ShowFilename = "on"
788 } else {
789 o.ShowFilename = "off"
790 }
791 } else {
792 o.ShowFilename = "on"
793 }
794 }
795
796 if o.Color == "auto" {
797 // auto activate colored output only if STDOUT is a terminal
798 if o.Output == "" {
799 if runtime.GOOS != "windows" && terminal.IsTerminal(int(os.Stdout.Fd())) {
800 o.Color = "on"
801 } else {
802 o.Color = "off"
803 }
804 } else {
805 o.Color = "off"
806 }
807 }
808
809 if o.GroupByFile {
810 if !terminal.IsTerminal(int(os.Stdout.Fd())) {
811 o.GroupByFile = false
812 }
813 }
814
815 if !o.IgnoreCase && o.SmartCase {
816 if len(patterns) >= 1 {
817 if m, _ := regexp.MatchString("[A-Z]", patterns[0]); !m {
818 o.IgnoreCase = true
819 }
820 }
821 }
822
823 if o.Cores == 0 {
824 o.Cores = runtime.NumCPU()
825 }
826
827 if o.Color == "on" {
828 global.termHighlightFilename = fmt.Sprintf("\033[%d;%d;%dm", 1, 35, 49)
829 global.termHighlightLineno = fmt.Sprintf("\033[%d;%d;%dm", 1, 32, 49)
830 global.termHighlightMatch = fmt.Sprintf("\033[%d;%d;%dm", 1, 31, 49)
831 global.termHighlightReset = fmt.Sprintf("\033[%d;%d;%dm", 0, 39, 49)
832 } else {
833 global.termHighlightFilename = ""
834 global.termHighlightLineno = ""
835 global.termHighlightMatch = ""
836 global.termHighlightReset = ""
837 }
838 }