package bonfire import ( "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" ) // maxVerbosity caps -v, -vv and -vvv (and repeated --verbose). const maxVerbosity = 3 // Globals are the global console options every command accepts: -v, -vv and // -vvv (--verbose), -q (--quiet), --ansi and --no-ansi, -n // (--no-interaction) and --env. type Globals struct { // Verbosity is 0 by default and 1 to 3 for -v, -vv and -vvv. Verbosity int // Quiet suppresses decoration and info lines; it implies NoInteraction. Quiet bool // Color is ColorAlways for --ansi, ColorNever for --no-ansi and // ColorAuto otherwise; the last of the two flags wins. Color ColorMode // NoInteraction makes prompts return their defaults without asking. NoInteraction bool // Env is the --env value: the environment the command runs under. Env string } // Global option names and shorthands, reserved on every command. const ( flagVerbose = "verbose" flagQuiet = "quiet" flagANSI = "ansi" flagNoANSI = "no-ansi" flagNoInteraction = "no-interaction" flagEnv = "env" flagHelp = "help" ) var ( reservedFlagNames = []string{flagVerbose, flagQuiet, flagANSI, flagNoANSI, flagNoInteraction, flagEnv, flagHelp} reservedFlagShorthands = []string{"v", "q", "n", "h"} ) // PeekGlobals reads the global options from args before the command root // exists, so the generated main can pick the configuration environment and // the log verbosity before config loads and plugins boot. The scan is // tolerant: it never fails, ignores every other token and stops at "--". // Quiet also sets NoInteraction. func PeekGlobals(args []string) Globals { var g Globals for i := 0; i < len(args); i++ { a := args[i] switch { case a == "--": return g.normalize() case a == "--"+flagVerbose: g.Verbosity++ case a == "-q" || a == "--"+flagQuiet: g.Quiet = true case a == "-n" || a == "--"+flagNoInteraction: g.NoInteraction = true case a == "--"+flagANSI: g.Color = ColorAlways case a == "--"+flagNoANSI: g.Color = ColorNever case a == "--"+flagEnv: if i+1 < len(args) { g.Env = args[i+1] i++ } case strings.HasPrefix(a, "--"+flagEnv+"="): g.Env = strings.TrimPrefix(a, "--"+flagEnv+"=") case isVerboseShorthand(a): g.Verbosity += len(a) - 1 } } return g.normalize() } func isVerboseShorthand(a string) bool { if len(a) < 2 || a[0] != '-' || a[1] == '-' { return false } return strings.Trim(a[1:], "v") == "" } func (g Globals) normalize() Globals { g.Verbosity = min(max(g.Verbosity, 0), maxVerbosity) if g.Quiet { g.NoInteraction = true } return g } // Args re-serialises the globals as command-line flags, for forwarding to // another program: -v, -vv or -vvv, -q, -n, --ansi or --no-ansi and // --env=. The zero value gives no arguments. func (g Globals) Args() []string { var out []string if v := min(g.Verbosity, maxVerbosity); v > 0 { out = append(out, "-"+strings.Repeat("v", v)) } if g.Quiet { out = append(out, "-q") } if g.NoInteraction && !g.Quiet { out = append(out, "-n") } switch g.Color { case ColorAlways: out = append(out, "--"+flagANSI) case ColorNever: out = append(out, "--"+flagNoANSI) } if g.Env != "" { out = append(out, "--"+flagEnv+"="+g.Env) } return out } // globalFlags holds the parsed persistent flags of one root. type globalFlags struct { verbosity int quiet bool color ColorMode noInteraction bool env string } // colorFlag is --ansi or --no-ansi: both write one shared ColorMode, so the // last flag on the command line wins. type colorFlag struct { mode *ColorMode set ColorMode } func (f colorFlag) String() string { return strconv.FormatBool(*f.mode == f.set) } func (f colorFlag) Set(s string) error { on, err := strconv.ParseBool(s) if err != nil { return err } switch { case on: *f.mode = f.set case *f.mode == f.set: *f.mode = ColorAuto } return nil } func (f colorFlag) Type() string { return "bool" } // register adds the global options to the root's persistent flags. func (gf *globalFlags) register(fs *pflag.FlagSet) { fs.CountVarP(&gf.verbosity, flagVerbose, "v", "Increase verbosity (-v, -vv or -vvv)") fs.BoolVarP(&gf.quiet, flagQuiet, "q", false, "Do not output any message") fs.Var(colorFlag{mode: &gf.color, set: ColorAlways}, flagANSI, "Force ANSI output") fs.Lookup(flagANSI).NoOptDefVal = "true" fs.Var(colorFlag{mode: &gf.color, set: ColorNever}, flagNoANSI, "Disable ANSI output") fs.Lookup(flagNoANSI).NoOptDefVal = "true" fs.BoolVarP(&gf.noInteraction, flagNoInteraction, "n", false, "Do not ask any interactive question") fs.StringVar(&gf.env, flagEnv, "", "The environment the command runs under") } func (gf *globalFlags) globals() Globals { return Globals{ Verbosity: gf.verbosity, Quiet: gf.quiet, Color: gf.color, NoInteraction: gf.noInteraction, Env: gf.env, }.normalize() } // resetFlags puts every persistent flag of root back to its default, so a // root executed twice (tests, Execute) does not keep the previous run's // globals: pflag count flags add to their current value. func resetFlags(root *cobra.Command) { root.PersistentFlags().VisitAll(func(f *pflag.Flag) { _ = f.Value.Set(f.DefValue) f.Changed = false }) } // globalOption is one line of the global options block in list and help. type globalOption struct { flags string description string } // globalOptions are the global option lines of the list (D-13). var globalOptions = []globalOption{ {"-h, --help", "Display help for the given command"}, {"-q, --quiet", "Do not output any message"}, {"-v|vv|vvv", "Increase verbosity"}, {" --ansi|no-ansi", "Force (or disable) ANSI output"}, {"-n, --no-interaction", "Do not ask any interactive question"}, {" --env", "The environment the command runs under"}, }