"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 }