From 76d9b24d834bf9fd3e6bbd4a0488dfc071291b5d Mon Sep 17 00:00:00 2001 From: Matthew Mueller Date: Sun, 16 Apr 2023 19:08:29 -0500 Subject: [PATCH] bump zero --- example/zero/generator/app/generator.go | 2 +- example/zero/generator/command/generator.go | 2 +- example/zero/go.mod | 2 +- example/zero/go.sum | 2 + package/commander/commander.go | 102 -- package/commander/commander_test.go | 1050 ------------------- package/commander/subcommand.go | 174 --- package/commander/usage.go | 19 +- runtime/gen/gen.go | 4 +- 9 files changed, 14 insertions(+), 1343 deletions(-) delete mode 100644 package/commander/commander.go delete mode 100644 package/commander/commander_test.go delete mode 100644 package/commander/subcommand.go diff --git a/example/zero/generator/app/generator.go b/example/zero/generator/app/generator.go index 8af892f3..7572c7fa 100644 --- a/example/zero/generator/app/generator.go +++ b/example/zero/generator/app/generator.go @@ -123,5 +123,5 @@ func run(log log.Log, loadCLI loadCLI) error { return err } ctx := context.Background() - return cli.Parse(ctx, os.Args[1:]) + return cli.Parse(ctx, os.Args[1:]...) } diff --git a/example/zero/generator/command/generator.go b/example/zero/generator/command/generator.go index 56ee7146..8f799b4a 100644 --- a/example/zero/generator/command/generator.go +++ b/example/zero/generator/command/generator.go @@ -99,7 +99,7 @@ func (g *Generator) generateFile(fsys generator.FS, file *generator.File) error //////////////////////////////////////////////// func New(name string) *CLI { - return commander.New(name) + return commander.New(name, "zero app") } // Go starts a group of commands in goroutines and waits for them to finish diff --git a/example/zero/go.mod b/example/zero/go.mod index f345fb50..9c3897fd 100644 --- a/example/zero/go.mod +++ b/example/zero/go.mod @@ -10,7 +10,7 @@ require ( github.com/livebud/bud v0.0.0-00010101000000-000000000000 github.com/matryer/is v1.4.0 github.com/yuin/goldmark v1.5.4 - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/sync v0.1.0 ) require ( diff --git a/example/zero/go.sum b/example/zero/go.sum index 296b9bff..fb9db624 100644 --- a/example/zero/go.sum +++ b/example/zero/go.sum @@ -52,6 +52,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVD golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f h1:OKYpQQVE3DKSc3r3zHVzq46vq5YH7x8xpR3/k9ixmUg= diff --git a/package/commander/commander.go b/package/commander/commander.go deleted file mode 100644 index 533ed83b..00000000 --- a/package/commander/commander.go +++ /dev/null @@ -1,102 +0,0 @@ -package commander - -import ( - "context" - _ "embed" - "flag" - "io" - "os" - "text/template" - - "github.com/livebud/bud/internal/sig" -) - -type Command interface { - Command(name, usage string) Command - Hidden() Command - Flag(name, usage string) *Flag - Arg(name string) *Arg - Args(name string) *Args - Run(runner func(ctx context.Context) error) -} - -//go:embed usage.gotext -var usage string - -var defaultUsage = template.Must(template.New("usage").Funcs(colors).Parse(usage)) - -func Usage() error { - return flag.ErrHelp -} - -func New(name string) *CLI { - config := &config{"", os.Stdout, defaultUsage, []os.Signal{os.Interrupt}} - return &CLI{newSubcommand(config, name, ""), config} -} - -type CLI struct { - root *Subcommand - config *config -} - -var _ Command = (*CLI)(nil) - -type config struct { - version string - writer io.Writer - template *template.Template - signals []os.Signal -} - -func (c *CLI) Writer(writer io.Writer) *CLI { - c.config.writer = writer - return c -} - -func (c *CLI) Version(version string) *CLI { - c.config.version = version - return c -} - -func (c *CLI) Template(template *template.Template) { - c.config.template = template -} - -func (c *CLI) Trap(signals ...os.Signal) { - c.config.signals = signals -} - -func (c *CLI) Parse(ctx context.Context, args []string) error { - ctx = sig.Trap(ctx, c.config.signals...) - if err := c.root.parse(ctx, args); err != nil { - return err - } - // Give the caller a chance to handle context cancellations and therefore - // interrupts specifically. - return ctx.Err() -} - -func (c *CLI) Command(name, usage string) Command { - return c.root.Command(name, usage) -} - -// Hidden shouldn't hide the root command -func (c *CLI) Hidden() Command { - return c -} - -func (c *CLI) Flag(name, usage string) *Flag { - return c.root.Flag(name, usage) -} - -func (c *CLI) Arg(name string) *Arg { - return c.root.Arg(name) -} - -func (c *CLI) Args(name string) *Args { - return c.root.Args(name) -} - -func (c *CLI) Run(runner func(ctx context.Context) error) { - c.root.Run(runner) -} diff --git a/package/commander/commander_test.go b/package/commander/commander_test.go deleted file mode 100644 index 39b8da17..00000000 --- a/package/commander/commander_test.go +++ /dev/null @@ -1,1050 +0,0 @@ -package commander_test - -import ( - "bufio" - "bytes" - "context" - "errors" - "fmt" - "os" - "os/exec" - "strings" - "testing" - - "github.com/livebud/bud/internal/is" - "github.com/livebud/bud/package/commander" - "github.com/matthewmueller/diff" -) - -func isEqual(t testing.TB, actual, expected string) { - t.Helper() - equal(t, expected, replaceEscapeCodes(actual)) -} - -func replaceEscapeCodes(str string) string { - r := strings.NewReplacer( - "\033[0m", `{reset}`, - "\033[1m", `{bold}`, - "\033[37m", `{dim}`, - "\033[4m", `{underline}`, - "\033[36m", `{teal}`, - "\033[34m", `{blue}`, - "\033[33m", `{yellow}`, - "\033[31m", `{red}`, - "\033[32m", `{green}`, - ) - return r.Replace(str) -} - -// is checks if expect and actual are equal -func equal(t testing.TB, expect, actual string) { - t.Helper() - if expect == actual { - return - } - var b bytes.Buffer - b.WriteString("\n\x1b[4mExpect\x1b[0m:\n") - b.WriteString(expect) - b.WriteString("\n\n") - b.WriteString("\x1b[4mActual\x1b[0m: \n") - b.WriteString(actual) - b.WriteString("\n\n") - b.WriteString("\x1b[4mDifference\x1b[0m: \n") - b.WriteString(diff.String(expect, actual)) - b.WriteString("\n") - t.Fatal(b.String()) -} - -func TestHelp(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cmd := commander.New("cli").Writer(actual) - ctx := context.Background() - err := cmd.Parse(ctx, []string{"-h"}) - is.NoErr(err) - isEqual(t, actual.String(), ` - {bold}Usage:{reset} - cli - -`) -} - -func TestHelpArgs(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cmd := commander.New("cp").Writer(actual) - cmd.Arg("src").String(nil) - cmd.Arg("dst").String(nil).Default(".") - ctx := context.Background() - err := cmd.Parse(ctx, []string{"-h"}) - is.NoErr(err) - isEqual(t, actual.String(), ` - {bold}Usage:{reset} - cp {dim}{reset} {dim}{reset} - -`) -} - -func TestInvalid(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cmd := commander.New("cli").Writer(actual) - ctx := context.Background() - err := cmd.Parse(ctx, []string{"blargle"}) - is.Equal(err.Error(), "unexpected blargle") - isEqual(t, actual.String(), ``) -} -func TestSimple(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("cli").Writer(actual) - called := 0 - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(1, called) - isEqual(t, actual.String(), ``) -} -func TestFlagString(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag string - cli.Flag("flag", "cli flag").String(&flag) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--flag", "cool"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(flag, "cool") - isEqual(t, actual.String(), ``) -} -func TestFlagStringDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag string - cli.Flag("flag", "cli flag").String(&flag).Default("default") - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(flag, "default") - isEqual(t, actual.String(), ``) -} - -func TestFlagStringRequired(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag string - cli.Flag("flag", "cli flag").String(&flag) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.Equal(err.Error(), "missing --flag") -} -func TestFlagInt(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag int - cli.Flag("flag", "cli flag").Int(&flag) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--flag", "10"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(flag, 10) - isEqual(t, actual.String(), ``) -} -func TestFlagIntDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag int - cli.Flag("flag", "cli flag").Int(&flag).Default(10) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(flag, 10) - isEqual(t, actual.String(), ``) -} - -func TestFlagIntRequired(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag int - cli.Flag("flag", "cli flag").Int(&flag) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.Equal(err.Error(), "missing --flag") -} -func TestFlagBool(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag bool - cli.Flag("flag", "cli flag").Bool(&flag) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--flag"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(flag, true) - isEqual(t, actual.String(), ``) -} -func TestFlagBoolDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag bool - cli.Flag("flag", "cli flag").Bool(&flag).Default(true) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(flag, true) - isEqual(t, actual.String(), ``) -} - -func TestFlagBoolDefaultFalse(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag bool - cli.Flag("flag", "cli flag").Bool(&flag).Default(true) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--flag=false"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(flag, false) - isEqual(t, actual.String(), ``) -} - -func TestFlagBoolRequired(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flag bool - cli.Flag("flag", "cli flag").Bool(&flag) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.Equal(err.Error(), "missing --flag") -} - -func TestFlagStrings(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flags []string - cli.Flag("flag", "cli flag").Strings(&flags) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--flag", "1", "--flag", "2"}) - is.NoErr(err) - is.Equal(len(flags), 2) - is.Equal(flags[0], "1") - is.Equal(flags[1], "2") -} - -func TestFlagStringsRequired(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flags []string - cli.Flag("flag", "cli flag").Strings(&flags) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.Equal(err.Error(), "missing --flag") -} - -func TestFlagStringsDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flags []string - cli.Flag("flag", "cli flag").Strings(&flags).Default("a", "b") - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(len(flags), 2) - is.Equal(flags[0], "a") - is.Equal(flags[1], "b") -} - -func TestFlagStringMap(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flags map[string]string - cli.Flag("flag", "cli flag").StringMap(&flags) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--flag", "a:1 + 1", "--flag", "b:2"}) - is.NoErr(err) - is.Equal(len(flags), 2) - is.Equal(flags["a"], "1 + 1") - is.Equal(flags["b"], "2") -} - -func TestFlagStringMapRequired(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flags map[string]string - cli.Flag("flag", "cli flag").StringMap(&flags) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.Equal(err.Error(), "missing --flag") -} - -func TestFlagStringMapDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var flags map[string]string - cli.Flag("flag", "cli flag").StringMap(&flags).Default(map[string]string{ - "a": "1", - "b": "2", - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(len(flags), 2) - is.Equal(flags["a"], "1") - is.Equal(flags["b"], "2") -} - -func TestArgStringMap(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var args map[string]string - cli.Arg("arg").StringMap(&args) - // Can have only one arg - ctx := context.Background() - err := cli.Parse(ctx, []string{"a:1 + 1"}) - is.NoErr(err) - is.Equal(len(args), 1) - is.Equal(args["a"], "1 + 1") -} - -func TestArgStringMapRequired(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var args map[string]string - cli.Arg("arg").StringMap(&args) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.Equal(err.Error(), "missing arg") -} - -func TestArgStringMapDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var args map[string]string - cli.Arg("arg").StringMap(&args).Default(map[string]string{ - "a": "1", - "b": "2", - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(len(args), 2) - is.Equal(args["a"], "1") - is.Equal(args["b"], "2") -} - -func TestSub(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("bud").Writer(actual) - var trace []string - cli.Run(func(ctx context.Context) error { - trace = append(trace, "bud") - return nil - }) - { - sub := cli.Command("run", "run your application") - sub.Run(func(ctx context.Context) error { - trace = append(trace, "run") - return nil - }) - } - { - sub := cli.Command("build", "build your application") - sub.Run(func(ctx context.Context) error { - trace = append(trace, "build") - return nil - }) - } - ctx := context.Background() - err := cli.Parse(ctx, []string{"build"}) - is.NoErr(err) - is.Equal(len(trace), 1) - is.Equal(trace[0], "build") - isEqual(t, actual.String(), ``) -} - -func TestSubHelp(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("bud").Writer(actual) - cli.Flag("log", "specify the logger").Bool(nil) - cli.Command("run", "run your application") - cli.Command("build", "build your application") - ctx := context.Background() - err := cli.Parse(ctx, []string{"-h"}) - is.NoErr(err) - isEqual(t, actual.String(), ` - {bold}Usage:{reset} - bud {dim}[flags]{reset} {dim}[command]{reset} - - {bold}Flags:{reset} - --log {dim}specify the logger{reset} - - {bold}Commands:{reset} - build {dim}build your application{reset} - run {dim}run your application{reset} - -`) -} - -func TestEmptyUsage(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("bud").Writer(actual) - cli.Flag("log", "").Bool(nil) - cli.Command("run", "") - ctx := context.Background() - err := cli.Parse(ctx, []string{"-h"}) - is.NoErr(err) - isEqual(t, actual.String(), ` - {bold}Usage:{reset} - bud {dim}[flags]{reset} {dim}[command]{reset} - - {bold}Flags:{reset} - --log - - {bold}Commands:{reset} - run - -`) -} - -func TestSubHelpShort(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("bud").Writer(actual) - cli.Flag("log", "specify the logger").Short('L').Bool(nil).Default(false) - cli.Flag("debug", "set the debugger").Bool(nil).Default(true) - var trace []string - cli.Run(func(ctx context.Context) error { - trace = append(trace, "bud") - return nil - }) - { - sub := cli.Command("run", "run your application") - sub.Run(func(ctx context.Context) error { - trace = append(trace, "run") - return nil - }) - } - { - sub := cli.Command("build", "build your application") - sub.Run(func(ctx context.Context) error { - trace = append(trace, "build") - return nil - }) - } - ctx := context.Background() - err := cli.Parse(ctx, []string{"-h"}) - is.NoErr(err) - isEqual(t, actual.String(), ` - {bold}Usage:{reset} - bud {dim}[flags]{reset} {dim}[command]{reset} - - {bold}Flags:{reset} - -L, --log {dim}specify the logger{reset} - --debug {dim}set the debugger{reset} - - {bold}Commands:{reset} - build {dim}build your application{reset} - run {dim}run your application{reset} - -`) -} - -func TestArgString(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var arg string - cli.Arg("arg").String(&arg) - ctx := context.Background() - err := cli.Parse(ctx, []string{"cool"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(arg, "cool") - isEqual(t, actual.String(), ``) -} - -func TestArgStringDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var arg string - cli.Arg("arg").String(&arg).Default("default") - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(arg, "default") - isEqual(t, actual.String(), ``) -} - -func TestArgStringRequired(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var arg string - cli.Arg("arg").String(&arg) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.Equal(err.Error(), "missing arg") -} - -func TestSubArgString(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var arg string - cli.Command("build", "build command") - cli.Command("run", "run command") - cli.Arg("arg").String(&arg) - ctx := context.Background() - err := cli.Parse(ctx, []string{"deploy"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(arg, "deploy") - isEqual(t, actual.String(), ``) -} - -// TestInterrupt tests interrupts canceling context. It spawns a copy of itself -// to run a subcommand. I learned this trick from Mitchell Hashimoto's excellent -// "Advanced Testing with Go" talk. We use stdout to synchronize between the -// process and subprocess. -func TestInterrupt(t *testing.T) { - is := is.New(t) - if value := os.Getenv("TEST_INTERRUPT"); value == "" { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // Ignore -test.count otherwise this will continue recursively - var args []string - for _, arg := range os.Args[1:] { - if strings.HasPrefix(arg, "-test.count=") { - continue - } - args = append(args, arg) - } - cmd := exec.CommandContext(ctx, os.Args[0], append(args, "-test.v=true", "-test.run=^TestInterrupt$")...) - cmd.Env = append(os.Environ(), "TEST_INTERRUPT=1") - stdout, err := cmd.StdoutPipe() - is.NoErr(err) - cmd.Stderr = os.Stderr - is.NoErr(cmd.Start()) - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - line := scanner.Text() - if line == "ready" { - break - } - } - cmd.Process.Signal(os.Interrupt) - for scanner.Scan() { - line := scanner.Text() - if line == "cancelled" { - break - } - } - if err := cmd.Wait(); err != nil { - is.True(errors.Is(err, context.Canceled)) - } - return - } - cli := commander.New("cli") - cli.Run(func(ctx context.Context) error { - os.Stdout.Write([]byte("ready\n")) - <-ctx.Done() - os.Stdout.Write([]byte("cancelled\n")) - return nil - }) - ctx := context.Background() - if err := cli.Parse(ctx, []string{}); err != nil { - if errors.Is(err, context.Canceled) { - return - } - is.NoErr(err) - } -} - -// TODO: example support - -func TestArgsStrings(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - var args []string - cli.Command("build", "build command") - cli.Command("run", "run command") - cli.Args("custom").Strings(&args) - ctx := context.Background() - err := cli.Parse(ctx, []string{"new", "view"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(len(args), 2) - is.Equal(args[0], "new") - is.Equal(args[1], "view") - isEqual(t, actual.String(), ``) -} - -func TestUsageError(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return commander.Usage() - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - isEqual(t, actual.String(), ` - {bold}Usage:{reset} - cli - -`) -} - -func TestIdempotent(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("cli").Writer(actual) - var f1 string - cmd := cli.Command("run", "run command") - cmd.Flag("f1", "cli flag").Short('f').String(&f1) - var f2 string - cmd.Flag("f2", "cli flag").String(&f2) - var f3 string - cmd.Flag("f3", "cli flag").String(&f3) - ctx := context.Background() - args := []string{"run", "--f1=a", "--f2=b", "--f3", "c"} - err := cli.Parse(ctx, args) - is.NoErr(err) - is.Equal(f1, "a") - is.Equal(f2, "b") - is.Equal(f3, "c") - f1 = "" - f2 = "" - f3 = "" - err = cli.Parse(ctx, args) - is.NoErr(err) - is.Equal(f1, "a") - is.Equal(f2, "b") - is.Equal(f3, "c") -} - -func TestManualHelp(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("cli").Writer(actual) - var help bool - var dir string - cli.Flag("help", "help menu").Short('h').Bool(&help).Default(false) - cli.Flag("chdir", "change directory").Short('C').String(&dir) - called := 0 - cli.Run(func(ctx context.Context) error { - is.Equal(help, true) - is.Equal(dir, "somewhere") - called++ - return nil - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--help", "--chdir", "somewhere"}) - is.NoErr(err) - is.Equal(actual.String(), "") - is.Equal(called, 1) -} - -func TestManualHelpUsage(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("cli").Writer(actual) - var help bool - var dir string - cli.Flag("help", "help menu").Short('h').Bool(&help).Default(false) - cli.Flag("chdir", "change directory").Short('C').String(&dir) - called := 0 - cli.Run(func(ctx context.Context) error { - is.Equal(help, true) - is.Equal(dir, "somewhere") - called++ - return commander.Usage() - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--help", "--chdir", "somewhere"}) - is.NoErr(err) - is.Equal(called, 1) - isEqual(t, actual.String(), ` - {bold}Usage:{reset} - cli {dim}[flags]{reset} - - {bold}Flags:{reset} - -C, --chdir {dim}change directory{reset} - -h, --help {dim}help menu{reset} - -`) -} - -func TestAfterRun(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("cli").Writer(actual) - called := 0 - var ctx context.Context - cli.Run(func(c context.Context) error { - called++ - ctx = c - return nil - }) - err := cli.Parse(context.Background(), []string{}) - is.NoErr(err) - is.Equal(called, 1) - select { - case <-ctx.Done(): - is.Fail() // Context shouldn't have been cancelled - default: - } -} - -func TestArgsClearSlice(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - args := []string{"a", "b"} - cli.Args("custom").Strings(&args) - ctx := context.Background() - err := cli.Parse(ctx, []string{"c", "d"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(len(args), 2) - is.Equal(args[0], "c") - is.Equal(args[1], "d") - isEqual(t, actual.String(), ``) -} - -func TestArgClearMap(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - args := map[string]string{"a": "a"} - cli.Arg("custom").StringMap(&args) - ctx := context.Background() - err := cli.Parse(ctx, []string{"b:b"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(len(args), 1) - is.Equal(args["b"], "b") - isEqual(t, actual.String(), ``) -} - -func TestFlagClearSlice(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - args := []string{"a", "b"} - cli.Flag("f", "flag").Strings(&args) - ctx := context.Background() - err := cli.Parse(ctx, []string{"-f", "c", "-f", "d"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(len(args), 2) - is.Equal(args[0], "c") - is.Equal(args[1], "d") - isEqual(t, actual.String(), ``) -} - -func TestFlagClearMap(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - args := map[string]string{"a": "a"} - cli.Flag("f", "flag").StringMap(&args) - ctx := context.Background() - err := cli.Parse(ctx, []string{"-f", "b:b"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(len(args), 1) - is.Equal(args["b"], "b") - isEqual(t, actual.String(), ``) -} - -func TestFlagCustom(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - hot := "" - cli.Flag("hot", "hot server").Custom(func(v string) error { - hot = v - return nil - }) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--hot=:35729"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(hot, ":35729") -} - -func TestFlagCustomError(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Flag("hot", "hot server").Custom(func(v string) error { - return fmt.Errorf("unable to parse") - }) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--hot=:35729"}) - is.True(err != nil) - is.Equal(err.Error(), `invalid value ":35729" for flag -hot: unable to parse`) -} - -func TestFlagCustomMissing(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - cli.Flag("hot", "hot server").Custom(func(v string) error { - return nil - }) - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.True(err != nil) - is.Equal(err.Error(), `missing --hot`) -} - -func TestFlagCustomMissingDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - hot := "" - cli.Flag("hot", "hot server").Custom(func(v string) error { - hot = v - return nil - }).Default(":35729") - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(hot, ":35729") -} - -func TestFlagCustomDefault(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - called := 0 - cli := commander.New("cli").Writer(actual) - hot := "" - cli.Flag("hot", "hot server").Custom(func(v string) error { - hot = v - return nil - }).Default(":35729") - cli.Run(func(ctx context.Context) error { - called++ - return nil - }) - ctx := context.Background() - err := cli.Parse(ctx, []string{"--hot=false"}) - is.NoErr(err) - is.Equal(1, called) - is.Equal(hot, "false") -} - -func TestHiddenCommand(t *testing.T) { - is := is.New(t) - actual := new(bytes.Buffer) - cli := commander.New("cli").Writer(actual) - cli.Command("one", "one command").Hidden() - cli.Command("two", "two command") - ctx := context.Background() - err := cli.Parse(ctx, []string{"--help"}) - is.NoErr(err) - is.In(actual.String(), "cli") - is.In(actual.String(), "[command]") - is.In(actual.String(), "two") - is.In(actual.String(), "two command") - is.NotIn(actual.String(), "one") - is.NotIn(actual.String(), "one command") -} diff --git a/package/commander/subcommand.go b/package/commander/subcommand.go deleted file mode 100644 index 3229cf4b..00000000 --- a/package/commander/subcommand.go +++ /dev/null @@ -1,174 +0,0 @@ -package commander - -import ( - "context" - "errors" - "flag" - "fmt" - "io" -) - -func newSubcommand(config *config, name, usage string) *Subcommand { - fset := flag.NewFlagSet(name, flag.ContinueOnError) - fset.SetOutput(io.Discard) - return &Subcommand{ - config: config, - fset: fset, - name: name, - usage: usage, - commands: map[string]*Subcommand{}, - } -} - -type Subcommand struct { - config *config - fset *flag.FlagSet - run func(ctx context.Context) error - parsed bool - - // state for the template - name string - usage string - hidden bool - commands map[string]*Subcommand - flags []*Flag - args []*Arg - restArgs *Args // optional, collects the rest of the args -} - -var _ Command = (*Subcommand)(nil) - -func (c *Subcommand) printUsage() error { - usage, err := generateUsage(c.config.template, c) - if err != nil { - return err - } - fmt.Fprint(c.config.writer, usage) - return nil -} - -type value interface { - flag.Value - verify(displayName string) error -} - -// Set flags only once -func (c *Subcommand) setFlags() { - if c.parsed { - return - } - c.parsed = true - for _, flag := range c.flags { - c.fset.Var(flag.value, flag.name, flag.usage) - if flag.short != 0 { - c.fset.Var(flag.value, string(flag.short), flag.usage) - } - } -} - -func (c *Subcommand) parse(ctx context.Context, args []string) error { - // Set flags - c.setFlags() - // Parse the arguments - if err := c.fset.Parse(args); err != nil { - // Print usage if the developer used -h or --help - if errors.Is(err, flag.ErrHelp) { - return c.printUsage() - } - return err - } - // Verify that all the flags have been set or have default values - if err := verifyFlags(c.flags); err != nil { - return err - } - // Check if the first argument is a subcommand - if sub, ok := c.commands[c.fset.Arg(0)]; ok { - return sub.parse(ctx, c.fset.Args()[1:]) - } - // Handle the remaining arguments - numArgs := len(c.args) - restArgs := c.fset.Args() -loop: - for i, arg := range restArgs { - if i >= numArgs { - if c.restArgs == nil { - return fmt.Errorf("unexpected %s", arg) - } - // Loop over the remaining unset args, appending them to restArgs - for _, arg := range restArgs[i:] { - c.restArgs.value.Set(arg) - } - break loop - } - if err := c.args[i].value.Set(arg); err != nil { - return err - } - } - // Verify that all the args have been set or have default values - if err := verifyArgs(c.args); err != nil { - return err - } - // Print usage if there's no run function defined - if c.run == nil { - if len(restArgs) == 0 { - return c.printUsage() - } - return fmt.Errorf("unexpected %s", c.fset.Arg(0)) - } - if err := c.run(ctx); err != nil { - // Support explicitly printing usage - if errors.Is(err, flag.ErrHelp) { - return c.printUsage() - } - return err - } - return nil -} - -func (c *Subcommand) Run(runner func(ctx context.Context) error) { - c.run = runner -} - -func (c *Subcommand) Hidden() Command { - c.hidden = true - return c -} - -func (c *Subcommand) Command(name, usage string) Command { - if c.commands[name] != nil { - return c.commands[name] - } - cmd := newSubcommand(c.config, name, usage) - c.commands[name] = cmd - return cmd -} - -func (c *Subcommand) Arg(name string) *Arg { - arg := &Arg{ - Name: name, - } - c.args = append(c.args, arg) - return arg -} - -func (c *Subcommand) Args(name string) *Args { - if c.restArgs != nil { - // Panic is okay here because settings commands should be done during - // initialization. We want to fail fast for invalid usage. - panic("commander: you can only use cmd.Args(name, usage) once per command") - } - args := &Args{ - Name: name, - } - c.restArgs = args - return args -} - -func (c *Subcommand) Flag(name, usage string) *Flag { - flag := &Flag{ - name: name, - usage: usage, - } - c.flags = append(c.flags, flag) - return flag -} diff --git a/package/commander/usage.go b/package/commander/usage.go index 951344de..26de11d0 100644 --- a/package/commander/usage.go +++ b/package/commander/usage.go @@ -28,18 +28,13 @@ func (u *usage) Name() string { return u.root.name } -type generateCommands []*generateCommand - -func (cmds generateCommands) Usage() (string, error) { - buf := new(bytes.Buffer) - tw := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) - for _, cmd := range cmds { - if cmd.c.hidden { - continue - } - tw.Write([]byte("\t\t" + cmd.c.name)) - if cmd.c.usage != "" { - tw.Write([]byte("\t" + dim() + cmd.c.usage + reset())) +func (u *usage) Usage() string { + out := new(strings.Builder) + out.WriteString(u.cmd.full) + if u.cmd.run == nil && len(u.cmd.commands) > 0 { + out.WriteString(dim()) + if u.cmd.full != "" { + out.WriteString(":") } out.WriteString("command") out.WriteString(reset()) diff --git a/runtime/gen/gen.go b/runtime/gen/gen.go index 21498b1e..7bc4510c 100644 --- a/runtime/gen/gen.go +++ b/runtime/gen/gen.go @@ -49,13 +49,13 @@ func Main(load loadFn) { func run(ctx context.Context, load loadFn, args []string) error { cmd := &Generate{new(framework.Flag), "info", load} - cli := commander.New("gen") + cli := commander.New("gen", "generator") cli.Flag("embed", "embed assets").Bool(&cmd.flag.Embed).Default(false) cli.Flag("hot", "hot reloading").Bool(&cmd.flag.Hot).Default(true) cli.Flag("minify", "minify assets").Bool(&cmd.flag.Minify).Default(false) cli.Flag("log", "filter logs with this pattern").Short('L').String(&cmd.lvl).Default("info") cli.Run(cmd.Run) - return cli.Parse(ctx, args) + return cli.Parse(ctx, args...) } // Generate command