Skip to content

Commit afc4b43

Browse files
committed
#66: properly merge (not overwrite) slice flags from config and command-line
1 parent 9c07341 commit afc4b43

File tree

6 files changed

+531
-199
lines changed

6 files changed

+531
-199
lines changed

pkg/commands/root.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import (
77
"runtime"
88
"runtime/pprof"
99

10+
"github.com/golangci/golangci-lint/pkg/config"
1011
"github.com/golangci/golangci-lint/pkg/printers"
1112
"github.com/sirupsen/logrus"
1213
"github.com/spf13/cobra"
1314
"github.com/spf13/pflag"
1415
)
1516

16-
func (e *Executor) setupLog() {
17+
func setupLog(isVerbose bool) {
1718
log.SetFlags(0) // don't print time
18-
if e.cfg.Run.IsVerbose {
19+
if isVerbose {
1920
logrus.SetLevel(logrus.InfoLevel)
2021
}
2122
}
@@ -28,7 +29,7 @@ func (e *Executor) persistentPreRun(cmd *cobra.Command, args []string) {
2829

2930
runtime.GOMAXPROCS(e.cfg.Run.Concurrency)
3031

31-
e.setupLog()
32+
setupLog(e.cfg.Run.IsVerbose)
3233

3334
if e.cfg.Run.CPUProfilePath != "" {
3435
f, err := os.Create(e.cfg.Run.CPUProfilePath)
@@ -81,16 +82,16 @@ func (e *Executor) initRoot() {
8182
PersistentPostRun: e.persistentPostRun,
8283
}
8384

84-
e.initRootFlagSet(rootCmd.PersistentFlags())
85+
initRootFlagSet(rootCmd.PersistentFlags(), e.cfg, e.needVersionOption())
8586
e.rootCmd = rootCmd
8687
}
8788

88-
func (e *Executor) initRootFlagSet(fs *pflag.FlagSet) {
89-
fs.BoolVarP(&e.cfg.Run.IsVerbose, "verbose", "v", false, wh("verbose output"))
90-
fs.StringVar(&e.cfg.Run.CPUProfilePath, "cpu-profile-path", "", wh("Path to CPU profile output file"))
91-
fs.StringVar(&e.cfg.Run.MemProfilePath, "mem-profile-path", "", wh("Path to memory profile output file"))
92-
fs.IntVarP(&e.cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(), wh("Concurrency (default NumCPU)"))
93-
if e.date != "" {
94-
fs.BoolVar(&e.cfg.Run.PrintVersion, "version", false, wh("Print version"))
89+
func initRootFlagSet(fs *pflag.FlagSet, cfg *config.Config, needVersionOption bool) {
90+
fs.BoolVarP(&cfg.Run.IsVerbose, "verbose", "v", false, wh("verbose output"))
91+
fs.StringVar(&cfg.Run.CPUProfilePath, "cpu-profile-path", "", wh("Path to CPU profile output file"))
92+
fs.StringVar(&cfg.Run.MemProfilePath, "mem-profile-path", "", wh("Path to memory profile output file"))
93+
fs.IntVarP(&cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(), wh("Concurrency (default NumCPU)"))
94+
if needVersionOption {
95+
fs.BoolVar(&cfg.Run.PrintVersion, "version", false, wh("Print version"))
9596
}
9697
}

pkg/commands/run.go

Lines changed: 18 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,17 @@ package commands
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"go/token"
87
"io/ioutil"
98
"log"
109
"os"
11-
"path/filepath"
1210
"runtime"
1311
"strings"
1412
"time"
1513

1614
"github.com/fatih/color"
1715
"github.com/golangci/golangci-lint/pkg/config"
18-
"github.com/golangci/golangci-lint/pkg/fsutils"
1916
"github.com/golangci/golangci-lint/pkg/lint"
2017
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
2118
"github.com/golangci/golangci-lint/pkg/printers"
@@ -24,7 +21,6 @@ import (
2421
"github.com/sirupsen/logrus"
2522
"github.com/spf13/cobra"
2623
"github.com/spf13/pflag"
27-
"github.com/spf13/viper"
2824
)
2925

3026
const (
@@ -48,15 +44,15 @@ func wh(text string) string {
4844
return color.GreenString(text)
4945
}
5046

51-
func (e *Executor) initFlagSet(fs *pflag.FlagSet) {
47+
func initFlagSet(fs *pflag.FlagSet, cfg *config.Config) {
5248
hideFlag := func(name string) {
5349
if err := fs.MarkHidden(name); err != nil {
5450
panic(err)
5551
}
5652
}
5753

5854
// Output config
59-
oc := &e.cfg.Output
55+
oc := &cfg.Output
6056
fs.StringVar(&oc.Format, "out-format",
6157
config.OutFormatColoredLineNumber,
6258
wh(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|"))))
@@ -66,18 +62,18 @@ func (e *Executor) initFlagSet(fs *pflag.FlagSet) {
6662
hideFlag("print-welcome") // no longer used
6763

6864
// Run config
69-
rc := &e.cfg.Run
65+
rc := &cfg.Run
7066
fs.IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code",
7167
1, wh("Exit code when issues were found"))
72-
fs.StringSliceVar(&rc.BuildTags, "build-tags", []string{}, wh("Build tags (not all linters support them)"))
68+
fs.StringSliceVar(&rc.BuildTags, "build-tags", nil, wh("Build tags (not all linters support them)"))
7369
fs.DurationVar(&rc.Deadline, "deadline", time.Minute, wh("Deadline for total work"))
7470
fs.BoolVar(&rc.AnalyzeTests, "tests", true, wh("Analyze tests (*_test.go)"))
7571
fs.BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false, wh("Print avg and max memory usage of golangci-lint and total time"))
7672
fs.StringVarP(&rc.Config, "config", "c", "", wh("Read config from file path `PATH`"))
7773
fs.BoolVar(&rc.NoConfig, "no-config", false, wh("Don't read config"))
7874

7975
// Linters settings config
80-
lsc := &e.cfg.LintersSettings
76+
lsc := &cfg.LintersSettings
8177

8278
// Hide all linters settings flags: they were initially visible,
8379
// but when number of linters started to grow it became ovious that
@@ -126,18 +122,18 @@ func (e *Executor) initFlagSet(fs *pflag.FlagSet) {
126122
hideFlag("depguard.include-go-root")
127123

128124
// Linters config
129-
lc := &e.cfg.Linters
130-
fs.StringSliceVarP(&lc.Enable, "enable", "E", []string{}, wh("Enable specific linter"))
131-
fs.StringSliceVarP(&lc.Disable, "disable", "D", []string{}, wh("Disable specific linter"))
125+
lc := &cfg.Linters
126+
fs.StringSliceVarP(&lc.Enable, "enable", "E", nil, wh("Enable specific linter"))
127+
fs.StringSliceVarP(&lc.Disable, "disable", "D", nil, wh("Disable specific linter"))
132128
fs.BoolVar(&lc.EnableAll, "enable-all", false, wh("Enable all linters"))
133129
fs.BoolVar(&lc.DisableAll, "disable-all", false, wh("Disable all linters"))
134-
fs.StringSliceVarP(&lc.Presets, "presets", "p", []string{},
130+
fs.StringSliceVarP(&lc.Presets, "presets", "p", nil,
135131
wh(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint linters' to see them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|"))))
136132
fs.BoolVar(&lc.Fast, "fast", false, wh("Run only fast linters from enabled linters set"))
137133

138134
// Issues config
139-
ic := &e.cfg.Issues
140-
fs.StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", []string{}, wh("Exclude issue by regexp"))
135+
ic := &cfg.Issues
136+
fs.StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", nil, wh("Exclude issue by regexp"))
141137
fs.BoolVar(&ic.UseDefaultExcludes, "exclude-use-default", true, getDefaultExcludeHelp())
142138

143139
fs.IntVar(&ic.MaxIssuesPerLinter, "max-issues-per-linter", 50, wh("Maximum issues count per one linter. Set to 0 to disable"))
@@ -162,9 +158,15 @@ func (e *Executor) initRun() {
162158

163159
fs := runCmd.Flags()
164160
fs.SortFlags = false // sort them as they are defined here
165-
e.initFlagSet(fs)
161+
initFlagSet(fs, e.cfg)
166162

163+
// init e.cfg by values from config: flags parse will see these values
164+
// like the default ones. It will overwrite them only if the same option
165+
// is found in command-line: it's ok, command-line has higher priority.
167166
e.parseConfig()
167+
168+
// Slice options must be explicitly set for properly merging.
169+
fixSlicesFlags(fs)
168170
}
169171

170172
func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan result.Issue, error) {
@@ -289,172 +291,6 @@ func (e *Executor) executeRun(cmd *cobra.Command, args []string) {
289291
}
290292
}
291293

292-
func (e *Executor) parseConfig() {
293-
// XXX: hack with double parsing for 2 purposes:
294-
// 1. to access "config" option here.
295-
// 2. to give config less priority than command line.
296-
297-
// We use another pflag.FlagSet here to not set `changed` flag
298-
// on cmd.Flags() options. Otherwise string slice options will be duplicated.
299-
fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)
300-
301-
// Don't do `fs.AddFlagSet(cmd.Flags())` because it shared flags representations:
302-
// `changed` variable inside string slice vars will be shared.
303-
e.initFlagSet(fs)
304-
e.initRootFlagSet(fs)
305-
306-
fs.Usage = func() {} // otherwise help text will be printed twice
307-
if err := fs.Parse(os.Args); err != nil {
308-
if err == pflag.ErrHelp {
309-
return
310-
}
311-
logrus.Fatalf("Can't parse args: %s", err)
312-
}
313-
314-
e.setupLog() // for `-v` to work until running of preRun function
315-
316-
if err := viper.BindPFlags(fs); err != nil {
317-
logrus.Fatalf("Can't bind cobra's flags to viper: %s", err)
318-
}
319-
320-
viper.SetEnvPrefix("GOLANGCI")
321-
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
322-
viper.AutomaticEnv()
323-
324-
configFile := e.cfg.Run.Config
325-
if e.cfg.Run.NoConfig && configFile != "" {
326-
logrus.Fatal("can't combine option --config and --no-config")
327-
}
328-
329-
if e.cfg.Run.NoConfig {
330-
return
331-
}
332-
333-
if configFile != "" {
334-
viper.SetConfigFile(configFile)
335-
} else {
336-
setupConfigFileSearch(fs.Args())
337-
}
338-
339-
e.parseConfigImpl()
340-
}
341-
342-
func setupConfigFileSearch(args []string) {
343-
// skip all args ([golangci-lint, run/linters]) before files/dirs list
344-
for len(args) != 0 {
345-
if args[0] == "run" {
346-
args = args[1:]
347-
break
348-
}
349-
350-
args = args[1:]
351-
}
352-
353-
// find first file/dir arg
354-
firstArg := "./..."
355-
if len(args) != 0 {
356-
firstArg = args[0]
357-
}
358-
359-
absStartPath, err := filepath.Abs(firstArg)
360-
if err != nil {
361-
logrus.Infof("Can't make abs path for %q: %s", firstArg, err)
362-
absStartPath = filepath.Clean(firstArg)
363-
}
364-
365-
// start from it
366-
var curDir string
367-
if fsutils.IsDir(absStartPath) {
368-
curDir = absStartPath
369-
} else {
370-
curDir = filepath.Dir(absStartPath)
371-
}
372-
373-
// find all dirs from it up to the root
374-
configSearchPaths := []string{"./"}
375-
for {
376-
configSearchPaths = append(configSearchPaths, curDir)
377-
newCurDir := filepath.Dir(curDir)
378-
if curDir == newCurDir || newCurDir == "" {
379-
break
380-
}
381-
curDir = newCurDir
382-
}
383-
384-
logrus.Infof("Config search paths: %s", configSearchPaths)
385-
viper.SetConfigName(".golangci")
386-
for _, p := range configSearchPaths {
387-
viper.AddConfigPath(p)
388-
}
389-
}
390-
391-
func getRelPath(p string) string {
392-
wd, err := os.Getwd()
393-
if err != nil {
394-
logrus.Infof("Can't get wd: %s", err)
395-
return p
396-
}
397-
398-
r, err := filepath.Rel(wd, p)
399-
if err != nil {
400-
logrus.Infof("Can't make path %s relative to %s: %s", p, wd, err)
401-
return p
402-
}
403-
404-
return r
405-
}
406-
407-
func (e *Executor) parseConfigImpl() {
408-
commandLineConfig := *e.cfg // make copy
409-
410-
if err := viper.ReadInConfig(); err != nil {
411-
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
412-
return
413-
}
414-
logrus.Fatalf("Can't read viper config: %s", err)
415-
}
416-
417-
usedConfigFile := viper.ConfigFileUsed()
418-
if usedConfigFile == "" {
419-
return
420-
}
421-
logrus.Infof("Used config file %s", getRelPath(usedConfigFile))
422-
423-
if err := viper.Unmarshal(&e.cfg); err != nil {
424-
logrus.Fatalf("Can't unmarshal config by viper: %s", err)
425-
}
426-
427-
if err := e.validateConfig(&commandLineConfig); err != nil {
428-
logrus.Fatal(err)
429-
}
430-
431-
if e.cfg.InternalTest { // just for testing purposes: to detect config file usage
432-
fmt.Fprintln(printers.StdOut, "test")
433-
os.Exit(0)
434-
}
435-
}
436-
437-
func (e *Executor) validateConfig(commandLineConfig *config.Config) error {
438-
c := e.cfg
439-
if len(c.Run.Args) != 0 {
440-
return errors.New("option run.args in config isn't supported now")
441-
}
442-
443-
if commandLineConfig.Run.CPUProfilePath == "" && c.Run.CPUProfilePath != "" {
444-
return errors.New("option run.cpuprofilepath in config isn't allowed")
445-
}
446-
447-
if commandLineConfig.Run.MemProfilePath == "" && c.Run.MemProfilePath != "" {
448-
return errors.New("option run.memprofilepath in config isn't allowed")
449-
}
450-
451-
if !commandLineConfig.Run.IsVerbose && c.Run.IsVerbose {
452-
return errors.New("can't set run.verbose option with config: only on command-line")
453-
}
454-
455-
return nil
456-
}
457-
458294
func watchResources(ctx context.Context, done chan struct{}) {
459295
startedAt := time.Now()
460296

0 commit comments

Comments
 (0)