2024-03-19 21:35:21 +01:00

168 lines
4.1 KiB
Go

package commands
import (
"errors"
"fmt"
"os"
"slices"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/golangci/golangci-lint/pkg/logutils"
)
func Execute(info BuildInfo) error {
return newRootCommand(info).Execute()
}
type rootOptions struct {
PrintVersion bool // Flag only.
Verbose bool // Flag only.
Color string // Flag only.
}
type rootCommand struct {
cmd *cobra.Command
opts rootOptions
log logutils.Log
}
func newRootCommand(info BuildInfo) *rootCommand {
c := &rootCommand{}
rootCmd := &cobra.Command{
Use: "golangci-lint",
Short: "golangci-lint is a smart linters runner.",
Long: `Smart, fast linters runner.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if c.opts.PrintVersion {
_ = printVersion(logutils.StdOut, info)
return nil
}
return cmd.Help()
},
}
fs := rootCmd.Flags()
fs.BoolVar(&c.opts.PrintVersion, "version", false, color.GreenString("Print version"))
setupRootPersistentFlags(rootCmd.PersistentFlags(), &c.opts)
log := logutils.NewStderrLog(logutils.DebugKeyEmpty)
// Each command uses a dedicated configuration structure to avoid side effects of bindings.
rootCmd.AddCommand(
newLintersCommand(log).cmd,
newRunCommand(log, info).cmd,
newCacheCommand().cmd,
newConfigCommand(log, info).cmd,
newVersionCommand(info).cmd,
newCustomCommand(log).cmd,
)
rootCmd.SetHelpCommand(newHelpCommand(log).cmd)
c.log = log
c.cmd = rootCmd
return c
}
func (c *rootCommand) Execute() error {
err := setupLogger(c.log)
if err != nil {
return err
}
return c.cmd.Execute()
}
func setupRootPersistentFlags(fs *pflag.FlagSet, opts *rootOptions) {
fs.BoolP("help", "h", false, color.GreenString("Help for a command"))
fs.BoolVarP(&opts.Verbose, "verbose", "v", false, color.GreenString("Verbose output"))
fs.StringVar(&opts.Color, "color", "auto", color.GreenString("Use color when printing; can be 'always', 'auto', or 'never'"))
}
func setupLogger(logger logutils.Log) error {
opts, err := forceRootParsePersistentFlags()
if err != nil && !errors.Is(err, pflag.ErrHelp) {
return err
}
if opts == nil {
return nil
}
logutils.SetupVerboseLog(logger, opts.Verbose)
switch opts.Color {
case "always":
color.NoColor = false
case "never":
color.NoColor = true
case "auto":
// nothing
default:
logger.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", opts.Color)
}
return nil
}
func forceRootParsePersistentFlags() (*rootOptions, error) {
// We use another pflag.FlagSet here to not set `changed` flag on cmd.Flags() options.
// Otherwise, string slice options will be duplicated.
fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)
// Ignore unknown flags because we will parse the command flags later.
fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
opts := &rootOptions{}
// Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations:
// `changed` variable inside string slice vars will be shared.
// Use another config variable here,
// to not affect main parsing by this parsing of only config option.
setupRootPersistentFlags(fs, opts)
fs.Usage = func() {} // otherwise, help text will be printed twice
if err := fs.Parse(safeArgs(fs, os.Args)); err != nil {
if errors.Is(err, pflag.ErrHelp) {
return nil, err
}
return nil, fmt.Errorf("can't parse args: %w", err)
}
return opts, nil
}
// Shorthands are a problem because pflag, with UnknownFlags, will try to parse all the letters as options.
// A shorthand can aggregate several letters (ex `ps -aux`)
// The function replaces non-supported shorthands by a dumb flag.
func safeArgs(fs *pflag.FlagSet, args []string) []string {
var shorthands []string
fs.VisitAll(func(flag *pflag.Flag) {
shorthands = append(shorthands, flag.Shorthand)
})
var cleanArgs []string
for _, arg := range args {
if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' && !slices.Contains(shorthands, string(arg[1])) {
cleanArgs = append(cleanArgs, "--potato")
continue
}
cleanArgs = append(cleanArgs, arg)
}
return cleanArgs
}