package bonfire import ( "strconv" "git.golem15.com/golem15/summercms/internal/consolekit" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" ) // maxVerbosity caps -v, -vv and -vvv (and repeated --verbose). const maxVerbosity = consolekit.MaxVerbosity // 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. The fields are Verbosity (0, or 1 to 3 for // -v to -vvv), Quiet (implies NoInteraction), Color (ColorAlways for // --ansi, ColorNever for --no-ansi, ColorAuto otherwise; the last of the // two flags wins), NoInteraction and Env. Its Args method re-serialises // them as flags for forwarding to another program. type Globals = consolekit.Globals // 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 "--". // It reads the =value forms the way pflag does (--verbose=2, -v=3, // --quiet=false, -n=1, --ansi=false), ignoring a value pflag would reject. // Quiet also sets NoInteraction. A value-taking command flag followed by a // global-looking token (--name -v) is read as the global before boot; the // command and its error block use cobra's reading. func PeekGlobals(args []string) Globals { var g Globals for i := 0; i < len(args); i++ { a := args[i] if name, value, ok := strings.Cut(a, "="); ok && strings.HasPrefix(name, "--") { switch strings.TrimPrefix(name, "--") { case flagVerbose: peekCount(&g.Verbosity, value) case flagQuiet: peekBool(&g.Quiet, value) case flagNoInteraction: peekBool(&g.NoInteraction, value) case flagANSI: peekColor(&g.Color, ColorAlways, value) case flagNoANSI: peekColor(&g.Color, ColorNever, value) case flagEnv: g.Env = value } continue } switch { case a == "--": return normalize(g) 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 isShorthandCluster(a): peekShorthands(&g, a[1:]) } } return normalize(g) } // peekCount applies a --verbose=N or -v=N value the way pflag's count flag // does: the value replaces the count; an unparsable value is ignored. func peekCount(n *int, value string) { if v, err := strconv.ParseInt(value, 0, 0); err == nil { *n = int(v) } } // peekBool applies a bool flag's =value the way pflag does; an unparsable // value is ignored. func peekBool(b *bool, value string) { if v, err := strconv.ParseBool(value); err == nil { *b = v } } // peekColor applies --ansi=B or --no-ansi=B the way colorFlag.Set does: // true sets the flag's mode, false resets the mode to ColorAuto only when it // is the flag's own; an unparsable value is ignored. func peekColor(mode *ColorMode, set ColorMode, value string) { on, err := strconv.ParseBool(value) switch { case err != nil: case on: *mode = set case *mode == set: *mode = ColorAuto } } // isShorthandCluster reports a single-dash token such as -v, -vvv or -qn. func isShorthandCluster(a string) bool { return len(a) >= 2 && a[0] == '-' && a[1] != '-' } // peekShorthands reads a cluster of shorthands the way pflag does (-qn is // -q -n, -vvq is -v -v -q, -v=3 sets the count, -qn=false turns -n off). It // stops at the first letter that is not a global shorthand: that may be a // command flag whose value is the rest of the token. func peekShorthands(g *Globals, cluster string) { for i := 0; i < len(cluster); i++ { r := cluster[i] if i+1 < len(cluster) && cluster[i+1] == '=' { value := cluster[i+2:] switch r { case 'v': peekCount(&g.Verbosity, value) case 'q': peekBool(&g.Quiet, value) case 'n': peekBool(&g.NoInteraction, value) } return } switch r { case 'v': g.Verbosity++ case 'q': g.Quiet = true case 'n': g.NoInteraction = true case 'h': default: return } } } // normalize caps the verbosity and lets Quiet imply NoInteraction. func normalize(g Globals) Globals { return consolekit.Normalize(g) } // 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 normalize(Globals{ Verbosity: gf.verbosity, Quiet: gf.quiet, Color: gf.color, NoInteraction: gf.noInteraction, Env: gf.env, }) } // 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"}, }