- Global options -v/-vv/-vvv, -q, --ansi/--no-ansi, -n and --env on every command; PeekGlobals reads them before config loads, OnGlobals hooks re-apply them after parsing (diary.ApplyGlobals in the generated main) - Bare binary, --help and list print the Artisan list with the version header from BuildVersion and namespace blocks - Execute runs the root through fang (no version, man page or completions) and returns 0, 1 or 2; Fail renders errors before the root exists - NewRoot rejects duplicate command names (ErrDuplicateCommand) and flags that reuse a global option (ErrReservedFlag) - Generated main and summer use PeekGlobals, compass.Open with --env, diary.FromGlobals and bonfire.Execute; docs checker knows the globals
207 lines
5.8 KiB
Go
207 lines
5.8 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 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=<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"},
|
|
}
|