diff --git a/command.go b/command.go index a9638aa..8f028e4 100644 --- a/command.go +++ b/command.go @@ -17,8 +17,10 @@ import ( ) const ( - helpBufferSize = 1024 // helpBufferSize is sufficient to hold most command --help text. - versionBufferSize = 256 // versionBufferSize is sufficient to hold all the --version text. + helpBufferSize = 1024 // helpBufferSize is sufficient to hold most command --help text. + versionBufferSize = 256 // versionBufferSize is sufficient to hold all the --version text. + defaultVersion = "dev" // defaultVersion is the version shown in --version when the user has not provided one. + defaultShort = "A placeholder for something cool" // defaultShort is the default value for cli.Short. ) // Builder is a function that constructs and returns a [Command], it makes constructing @@ -51,8 +53,8 @@ func New(name string, options ...Option) (*Command, error) { stderr: os.Stderr, args: os.Args[1:], name: name, - version: "dev", - short: "A placeholder for something cool", + version: defaultVersion, + short: defaultShort, argValidator: AnyArgs(), } @@ -218,7 +220,7 @@ func (cmd *Command) Execute() error { } if helpCalled { - if err := defaultHelp(cmd); err != nil { + if err := showHelp(cmd); err != nil { return fmt.Errorf("help function returned an error: %w", err) } @@ -234,8 +236,8 @@ func (cmd *Command) Execute() error { } if versionCalled { - if err := defaultVersion(cmd); err != nil { - return fmt.Errorf("version function returned an error: %w", err) + if err := showVersion(cmd); err != nil { + return fmt.Errorf("could not show version: %w", err) } return nil @@ -279,7 +281,7 @@ func (cmd *Command) Execute() error { // The only way we get here is if the command has subcommands defined but got no arguments given to it // so just show the usage and error - if err := defaultHelp(cmd); err != nil { + if err := showHelp(cmd); err != nil { return err } @@ -487,8 +489,8 @@ func stripFlags(cmd *Command, args []string) []string { return argsWithoutFlags } -// defaultHelp is the default for a command's helpFunc. -func defaultHelp(cmd *Command) error { +// showHelp is the default for a command's helpFunc. +func showHelp(cmd *Command) error { if cmd == nil { return errors.New("defaultHelp called on a nil Command") } @@ -580,6 +582,8 @@ func defaultHelp(cmd *Command) error { writeFooter(cmd, s) } + // Note: It's important to use cmd.Stderr() here over cmd.stderr + // as it resolves to the root's stderr fmt.Fprint(cmd.Stderr(), s.String()) return nil @@ -700,15 +704,22 @@ func writeFooter(cmd *Command, s *strings.Builder) { s.WriteByte('\n') } -// defaultVersion is the default for a command's versionFunc. -func defaultVersion(cmd *Command) error { +// showVersion is the default implementation of the --version flag. +func showVersion(cmd *Command) error { if cmd == nil { return errors.New("defaultVersion called on a nil Command") } + name := cmd.name // Incase we need to show the subcommand name + + if cmd.version == defaultVersion { + // User has not set a version for this command, so we show the root version info + cmd = cmd.root() + } + s := &strings.Builder{} s.Grow(versionBufferSize) - s.WriteString(style.Title.Text(cmd.name)) + s.WriteString(style.Title.Text(name)) s.WriteString("\n\n") s.WriteString(style.Bold.Text("Version:")) s.WriteString(" ") @@ -729,7 +740,9 @@ func defaultVersion(cmd *Command) error { s.WriteString("\n") } - fmt.Fprint(cmd.stderr, s.String()) + // Note: It's important to use cmd.Stderr() here over cmd.stderr + // as it resolves to the root's stderr + fmt.Fprint(cmd.Stderr(), s.String()) return nil } diff --git a/command_test.go b/command_test.go index 03b32b5..297079f 100644 --- a/command_test.go +++ b/command_test.go @@ -642,6 +642,31 @@ func TestHelp(t *testing.T) { } func TestVersion(t *testing.T) { + sub1 := func() (*cli.Command, error) { + return cli.New( + "sub1", + cli.Short("Do one thing"), + // No version set on sub1 + cli.Run(func(cmd *cli.Command, _ []string) error { + fmt.Fprintln(cmd.Stdout(), "Hello from sub1") + + return nil + })) + } + + sub2 := func() (*cli.Command, error) { + return cli.New( + "sub2", + cli.Short("Do another thing"), + cli.Version("sub2 version text"), + cli.Run(func(cmd *cli.Command, _ []string) error { + fmt.Fprintln(cmd.Stdout(), "Hello from sub2") + + return nil + }), + ) + } + tests := []struct { name string // Name of the test case stderr string // Expected output to stderr @@ -719,6 +744,34 @@ func TestVersion(t *testing.T) { stderr: "version-test\n\nVersion: v8.17.6\nCommit: b9aaafd\nBuildDate: 2024-08-17T10:37:30Z\n", wantErr: false, }, + { + name: "call on subcommand with no version", + options: []cli.Option{ + cli.OverrideArgs([]string{"sub1", "--version"}), + cli.Version("v8.17.6"), + cli.Commit("b9aaafd"), + cli.BuildDate("2024-08-17T10:37:30Z"), + cli.SubCommands(sub1, sub2), + cli.Run(func(cmd *cli.Command, args []string) error { return nil }), + }, + // Should show the root commands version info + stderr: "sub1\n\nVersion: v8.17.6\nCommit: b9aaafd\nBuildDate: 2024-08-17T10:37:30Z\n", + wantErr: false, + }, + { + name: "call on subcommand with version", + options: []cli.Option{ + cli.OverrideArgs([]string{"sub2", "--version"}), + cli.Version("v8.17.6"), + cli.Commit("b9aaafd"), + cli.BuildDate("2024-08-17T10:37:30Z"), + cli.SubCommands(sub1, sub2), + cli.Run(func(cmd *cli.Command, args []string) error { return nil }), + }, + // Should show sub2's version text + stderr: "sub2\n\nVersion: sub2 version text\n", + wantErr: false, + }, } for _, tt := range tests { @@ -733,7 +786,7 @@ func TestVersion(t *testing.T) { cli.NoColour(true), } - cmd, err := cli.New("version-test", slices.Concat(tt.options, options)...) + cmd, err := cli.New("version-test", slices.Concat(options, tt.options)...) test.Ok(t, err) err = cmd.Execute() diff --git a/examples/subcommands/cli.go b/examples/subcommands/cli.go index 0ae95e9..475588a 100644 --- a/examples/subcommands/cli.go +++ b/examples/subcommands/cli.go @@ -61,6 +61,7 @@ func buildDoCommand() (*cli.Command, error) { cli.Example("Do something", "demo do something --fast"), cli.Example("Do it 3 times", "demo do something --count 3"), cli.Example("Do it for a specific duration", "demo do something --duration 1m30s"), + cli.Version("do version"), cli.Allow(cli.ExactArgs(1)), // Only allowed to do one thing cli.Flag(&options.count, "count", 'c', 1, "Number of times to do the thing"), cli.Flag(&options.fast, "fast", 'f', false, "Do the thing quickly"),