feat(bonfire): global flags, Artisan list and exit codes
- 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
This commit is contained in:
@@ -96,7 +96,7 @@ func generateMain(m Manifest) ([]byte, error) {
|
||||
b.WriteString("// Code generated by summer build. DO NOT EDIT.\n\n")
|
||||
b.WriteString("package main\n\n")
|
||||
b.WriteString("import (\n")
|
||||
b.WriteString("\t\"fmt\"\n")
|
||||
b.WriteString("\t\"context\"\n")
|
||||
b.WriteString("\t\"io\"\n")
|
||||
b.WriteString("\t\"os\"\n\n")
|
||||
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/backpack\"\n")
|
||||
@@ -111,25 +111,19 @@ func generateMain(m Manifest) ([]byte, error) {
|
||||
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/surf\"\n")
|
||||
b.WriteString(")\n\n")
|
||||
b.WriteString("func main() {\n")
|
||||
b.WriteString("\tif err := run(os.Args[1:], os.Stdout); err != nil {\n")
|
||||
b.WriteString("\t\tfmt.Fprintln(os.Stderr, err)\n")
|
||||
b.WriteString("\t\tos.Exit(1)\n")
|
||||
b.WriteString("\t}\n")
|
||||
b.WriteString("\tos.Exit(run(os.Args[1:], os.Stdout))\n")
|
||||
b.WriteString("}\n\n")
|
||||
b.WriteString("func run(args []string, out io.Writer) error {\n")
|
||||
b.WriteString("\tdiary.Bootstrap(os.Stderr, diary.Options{})\n")
|
||||
b.WriteString("\tcfg, err := compass.Load(\"config\")\n")
|
||||
b.WriteString("\tif err != nil {\n")
|
||||
b.WriteString("\t\treturn err\n")
|
||||
b.WriteString("\t}\n")
|
||||
b.WriteString("\tif err := diary.Setup(cfg, diary.Options{}); err != nil {\n")
|
||||
b.WriteString("\t\treturn err\n")
|
||||
b.WriteString("\t}\n")
|
||||
binary := strconv.Quote(m.Binary)
|
||||
fail := "\t\treturn bonfire.Fail(os.Stderr, " + binary + ", err, g)\n"
|
||||
b.WriteString("func run(args []string, out io.Writer) int {\n")
|
||||
b.WriteString("\tg := bonfire.PeekGlobals(args)\n")
|
||||
b.WriteString("\tdiary.Bootstrap(os.Stderr, diary.FromGlobals(g))\n")
|
||||
b.WriteString("\tcfg, err := compass.Open(compass.Options{Dir: \"config\", Env: g.Env})\n")
|
||||
b.WriteString("\tif err != nil {\n" + fail + "\t}\n")
|
||||
b.WriteString("\tif err := diary.Setup(cfg, diary.FromGlobals(g)); err != nil {\n" + fail + "\t}\n")
|
||||
b.WriteString("\tapp := backpack.New(cfg)\n")
|
||||
b.WriteString("\tplugins, err := party.Activate(app, PluginIDs)\n")
|
||||
b.WriteString("\tif err != nil {\n")
|
||||
b.WriteString("\t\treturn err\n")
|
||||
b.WriteString("\t}\n")
|
||||
b.WriteString("\tif err != nil {\n" + fail + "\t}\n")
|
||||
b.WriteString("\tcommands := lagoon.RuntimeCommands(app, plugins)\n")
|
||||
b.WriteString("\tcommands = append(commands, conga.RuntimeCommands(app, plugins)...)\n")
|
||||
b.WriteString("\tcommands = append(commands, surf.ServeCommand(app, plugins))\n")
|
||||
@@ -140,15 +134,10 @@ func generateMain(m Manifest) ([]byte, error) {
|
||||
b.WriteString("\t\t\tcommands = append(commands, hasCommands.Commands()...)\n")
|
||||
b.WriteString("\t\t}\n")
|
||||
b.WriteString("\t}\n")
|
||||
b.WriteString("\tif err := app.Publish(bonfire.NewCatalog(commands)); err != nil {\n")
|
||||
b.WriteString("\t\treturn err\n")
|
||||
b.WriteString("\t}\n")
|
||||
fmt.Fprintf(&b, "\troot, err := bonfire.NewRoot(%s, commands, out)\n", strconv.Quote(m.Binary))
|
||||
b.WriteString("\tif err != nil {\n")
|
||||
b.WriteString("\t\treturn err\n")
|
||||
b.WriteString("\t}\n")
|
||||
b.WriteString("\troot.SetArgs(args)\n")
|
||||
b.WriteString("\treturn root.Execute()\n")
|
||||
b.WriteString("\tif err := app.Publish(bonfire.NewCatalog(commands)); err != nil {\n" + fail + "\t}\n")
|
||||
fmt.Fprintf(&b, "\troot, err := bonfire.NewRoot(%s, commands, out, bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))\n", binary)
|
||||
b.WriteString("\tif err != nil {\n" + fail + "\t}\n")
|
||||
b.WriteString("\treturn bonfire.Execute(context.Background(), root, args)\n")
|
||||
b.WriteString("}\n")
|
||||
return formatSource([]byte(b.String()))
|
||||
}
|
||||
|
||||
@@ -142,22 +142,26 @@ func TestGenerateMainRegistersCongaRuntimeCommands(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateMainInstallsDiary: the first statement of run installs the
|
||||
// diary console logger, before config loads or any plugin can log, and
|
||||
// TestGenerateMainInstallsDiary: run first reads the console global options
|
||||
// (bonfire.PeekGlobals), then installs the diary console logger with them,
|
||||
// before config loads (in the --env environment) or any plugin can log;
|
||||
// diary.Setup applies config/log.yaml right after compass loads it and before
|
||||
// the application container exists.
|
||||
// the application container exists, and the root re-applies the parsed
|
||||
// options to diary through bonfire.OnGlobals.
|
||||
func TestGenerateMainInstallsDiary(t *testing.T) {
|
||||
mainSrc, err := generateMain(Manifest{Module: "example.com/app", Binary: "hello"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bootstrap := []byte("diary.Bootstrap(os.Stderr, diary.Options{})")
|
||||
setup := []byte("diary.Setup(cfg, diary.Options{})")
|
||||
if n := bytes.Count(mainSrc, bootstrap); n != 1 {
|
||||
t.Fatalf("Bootstrap count = %d, want 1\n%s", n, mainSrc)
|
||||
}
|
||||
if n := bytes.Count(mainSrc, setup); n != 1 {
|
||||
t.Fatalf("Setup count = %d, want 1\n%s", n, mainSrc)
|
||||
peek := []byte("g := bonfire.PeekGlobals(args)")
|
||||
bootstrap := []byte("diary.Bootstrap(os.Stderr, diary.FromGlobals(g))")
|
||||
open := []byte(`compass.Open(compass.Options{Dir: "config", Env: g.Env})`)
|
||||
setup := []byte("diary.Setup(cfg, diary.FromGlobals(g))")
|
||||
hook := []byte("bonfire.OnGlobals(diary.ApplyGlobals)")
|
||||
for _, want := range [][]byte{peek, bootstrap, open, setup, hook, []byte("os.Exit(run(os.Args[1:], os.Stdout))"), []byte("return bonfire.Execute(context.Background(), root, args)")} {
|
||||
if n := bytes.Count(mainSrc, want); n != 1 {
|
||||
t.Fatalf("%s count = %d, want 1\n%s", want, n, mainSrc)
|
||||
}
|
||||
}
|
||||
if !bytes.Contains(mainSrc, []byte(`"git.golem15.com/golem15/summercms/modules/diary"`)) {
|
||||
t.Fatalf("generated main does not import diary:\n%s", mainSrc)
|
||||
@@ -165,17 +169,18 @@ func TestGenerateMainInstallsDiary(t *testing.T) {
|
||||
if bytes.Contains(mainSrc, []byte(`"git.golem15.com/golem15/summercms/modules/sunscreen"`)) {
|
||||
t.Fatalf("generated main still imports sunscreen:\n%s", mainSrc)
|
||||
}
|
||||
runSig := []byte("func run(args []string, out io.Writer) error {")
|
||||
runSig := []byte("func run(args []string, out io.Writer) int {")
|
||||
runStart := bytes.Index(mainSrc, runSig)
|
||||
pk := bytes.Index(mainSrc, peek)
|
||||
boot := bytes.Index(mainSrc, bootstrap)
|
||||
load := bytes.Index(mainSrc, []byte("compass.Load("))
|
||||
load := bytes.Index(mainSrc, open)
|
||||
set := bytes.Index(mainSrc, setup)
|
||||
newApp := bytes.Index(mainSrc, []byte("backpack.New("))
|
||||
if runStart < 0 || load < 0 || newApp < 0 || !(runStart < boot && boot < load && load < set && set < newApp) {
|
||||
t.Fatalf("want run { Bootstrap; compass.Load; Setup; backpack.New }:\n%s", mainSrc)
|
||||
if runStart < 0 || newApp < 0 || !(runStart < pk && pk < boot && boot < load && load < set && set < newApp) {
|
||||
t.Fatalf("want run { PeekGlobals; Bootstrap; compass.Open; Setup; backpack.New }:\n%s", mainSrc)
|
||||
}
|
||||
if between := bytes.TrimSpace(mainSrc[runStart+len(runSig) : boot]); len(between) != 0 {
|
||||
t.Fatalf("statements before Bootstrap: %q", between)
|
||||
if between := bytes.TrimSpace(mainSrc[runStart+len(runSig) : pk]); len(between) != 0 {
|
||||
t.Fatalf("statements before PeekGlobals: %q", between)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
internal/build/testdata/new/main.go.golden
vendored
31
internal/build/testdata/new/main.go.golden
vendored
@@ -3,7 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
@@ -20,25 +20,23 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:], os.Stdout); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(run(os.Args[1:], os.Stdout))
|
||||
}
|
||||
|
||||
func run(args []string, out io.Writer) error {
|
||||
diary.Bootstrap(os.Stderr, diary.Options{})
|
||||
cfg, err := compass.Load("config")
|
||||
func run(args []string, out io.Writer) int {
|
||||
g := bonfire.PeekGlobals(args)
|
||||
diary.Bootstrap(os.Stderr, diary.FromGlobals(g))
|
||||
cfg, err := compass.Open(compass.Options{Dir: "config", Env: g.Env})
|
||||
if err != nil {
|
||||
return err
|
||||
return bonfire.Fail(os.Stderr, "demo", err, g)
|
||||
}
|
||||
if err := diary.Setup(cfg, diary.Options{}); err != nil {
|
||||
return err
|
||||
if err := diary.Setup(cfg, diary.FromGlobals(g)); err != nil {
|
||||
return bonfire.Fail(os.Stderr, "demo", err, g)
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
plugins, err := party.Activate(app, PluginIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
return bonfire.Fail(os.Stderr, "demo", err, g)
|
||||
}
|
||||
commands := lagoon.RuntimeCommands(app, plugins)
|
||||
commands = append(commands, conga.RuntimeCommands(app, plugins)...)
|
||||
@@ -51,12 +49,11 @@ func run(args []string, out io.Writer) error {
|
||||
}
|
||||
}
|
||||
if err := app.Publish(bonfire.NewCatalog(commands)); err != nil {
|
||||
return err
|
||||
return bonfire.Fail(os.Stderr, "demo", err, g)
|
||||
}
|
||||
root, err := bonfire.NewRoot("demo", commands, out)
|
||||
root, err := bonfire.NewRoot("demo", commands, out, bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))
|
||||
if err != nil {
|
||||
return err
|
||||
return bonfire.Fail(os.Stderr, "demo", err, g)
|
||||
}
|
||||
root.SetArgs(args)
|
||||
return root.Execute()
|
||||
return bonfire.Execute(context.Background(), root, args)
|
||||
}
|
||||
|
||||
@@ -101,10 +101,12 @@ func (s *site) checkCommands(docs []parsedDoc) ([]Problem, error) {
|
||||
// command. tool is true for summer and `go run ./cmd/summer`, false for
|
||||
// ./bin/{app} and bin/{app}. ok is false when the line is not one of
|
||||
// those programs or when flags leave no command word. Flag handling
|
||||
// matches cobra's stripFlags for a root whose only flag is the bool
|
||||
// --help / -h: "--" ends the search, --help and -h take no value, any
|
||||
// other --name or two-character -x consumes the next token, and a token
|
||||
// that already contains "=" is skipped alone.
|
||||
// matches cobra's stripFlags for the bonfire root and its global options:
|
||||
// "--" ends the search; --help, -h, -v, -vv, -vvv, --verbose, -q, --quiet,
|
||||
// -n, --no-interaction, --ansi and --no-ansi take no value; --env takes
|
||||
// the next token (or --env=X); any other --name or two-character -x
|
||||
// consumes the next token, and a token that already contains "=" is
|
||||
// skipped alone.
|
||||
func commandWord(cmd string) (name string, tool bool, ok bool) {
|
||||
fields := strings.Fields(strings.TrimSpace(cmd))
|
||||
if len(fields) > 0 && fields[0] == "$" {
|
||||
@@ -134,7 +136,7 @@ func commandWord(cmd string) (name string, tool bool, ok bool) {
|
||||
return "", tool, false
|
||||
case strings.HasPrefix(s, "-") && strings.Contains(s, "="):
|
||||
fields = fields[1:]
|
||||
case s == "--help" || s == "-h":
|
||||
case valuelessGlobal(s):
|
||||
fields = fields[1:]
|
||||
case strings.HasPrefix(s, "--") || (strings.HasPrefix(s, "-") && len(s) == 2):
|
||||
if len(fields) < 3 {
|
||||
@@ -150,6 +152,18 @@ func commandWord(cmd string) (name string, tool bool, ok bool) {
|
||||
return "", tool, false
|
||||
}
|
||||
|
||||
// valuelessGlobal reports the root flags that take no value: --help and -h
|
||||
// and bonfire's bool global options. "--env" takes a value and is handled
|
||||
// with the other value flags.
|
||||
func valuelessGlobal(s string) bool {
|
||||
switch s {
|
||||
case "--help", "-h", "-v", "-vv", "-vvv", "--verbose", "-q", "--quiet",
|
||||
"-n", "--no-interaction", "--ansi", "--no-ansi":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isAssignment reports a leading VAR=value token: a name of letters,
|
||||
// digits and underscores that does not start with a digit.
|
||||
func isAssignment(tok string) bool {
|
||||
|
||||
@@ -593,6 +593,11 @@ func TestCommandWord(t *testing.T) {
|
||||
{"summer", "", tool, false},
|
||||
{"FOO=1", "", false, false},
|
||||
{"summer --version", "", tool, false},
|
||||
{"bin/acme -v serve", "serve", app, true},
|
||||
{"bin/acme -vvv --env=dev migrate", "migrate", app, true},
|
||||
{"bin/acme --env staging migrate", "migrate", app, true},
|
||||
{"summer -q --no-ansi -n list", "list", tool, true},
|
||||
{"bin/acme --verbose --quiet --no-interaction --ansi help migrate", "help", app, true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
name, isTool, ok := commandWord(tc.in)
|
||||
|
||||
Reference in New Issue
Block a user