Files
summercms/modules/bonfire/globals.go
Jakub Zych efb043e4ff fix(bonfire): PeekGlobals reads clustered shorthands like -qn and -vq
pflag accepts -qn as -q -n and -vvq as -v -v -q, but PeekGlobals only
read clusters made of v, so the generated main's pre-boot diary setup,
the error block of Execute and passthrough commands missed -q, -n and
-v when they were clustered. Read the cluster letter by letter and stop
at the first letter that is not a global shorthand.
2026-10-21 19:13:21 +02:00

225 lines
6.3 KiB
Go

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 isShorthandCluster(a):
g.peekShorthands(a[1:])
}
}
return g.normalize()
}
// 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). 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 (g *Globals) peekShorthands(cluster string) {
for _, r := range cluster {
switch r {
case 'v':
g.Verbosity++
case 'q':
g.Quiet = true
case 'n':
g.NoInteraction = true
case 'h':
default:
return
}
}
}
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=<value>. 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"},
}