diff --git a/CHANGELOG.md b/CHANGELOG.md index ed48809..cf5e8e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - cobra-extensions v0.4.0, 2024-11-27 + +### Added + +* Introduced the `NewMarkdownCommand` method to create a `cobra.Command` instance for generating Markdown documentation. This command can be linked to the root command and produces documentation for all registered commands and subcommands in Markdown format. + +### Changed + +* Enhanced the `CreateTypedCommand` function to support options, enabling greater flexibility in command creation and configuration. This update allows group commands to be marked as non-runnable, influencing their representation in Markdown documentation. To achieve this, pass the `NonRunnable` option to the `CreateTypedCommand` function. + +* Improved and expanded descriptions for example command groups and applications to enhance clarity and usability. + + ## [0.3.3] - cobra-extensions v0.3.3, 2024-11-27 ### Changed diff --git a/example/cmd/charmer/charmer.go b/example/cmd/charmer/charmer.go index 906121c..42b59e8 100644 --- a/example/cmd/charmer/charmer.go +++ b/example/cmd/charmer/charmer.go @@ -10,7 +10,7 @@ import ( func main() { err := - charmer.NewCommandLineApplication("charmer-example", ""). + charmer.NewCommandLineApplication("charmer-example", "A sample application to showcase the charmer package."). AddCommand(commands.CreateHelloCommand()). AddGroupCommand(commands.CreateCryptCommand(), func(crypto types.CommandSetup) { crypto.AddCommand( diff --git a/example/cmd/simple/main.go b/example/cmd/simple/main.go index aeed882..8ec28da 100644 --- a/example/cmd/simple/main.go +++ b/example/cmd/simple/main.go @@ -3,6 +3,7 @@ package main import ( "github.com/matzefriedrich/cobra-extensions/example/commands" "github.com/matzefriedrich/cobra-extensions/pkg/charmer" + builtin "github.com/matzefriedrich/cobra-extensions/pkg/commands" "os" "github.com/spf13/cobra" @@ -12,6 +13,7 @@ func main() { app := charmer.NewRootCommand("simple-example", "") + app.AddCommand(builtin.NewMarkdownDocsCommand(app)) app.AddCommand(commands.CreateHelloCommand()) AddCryptCommands(app) diff --git a/example/commands/crypto_commands.go b/example/commands/crypto_commands.go index 42fe038..a63feee 100644 --- a/example/commands/crypto_commands.go +++ b/example/commands/crypto_commands.go @@ -8,12 +8,13 @@ import ( type cryptoCommand struct { types.BaseCommand - use types.CommandName `flag:"crypt"` + use types.CommandName `flag:"crypt" short:"Provides commands to encrypt and decrypt messages." long:"The command group offers tools for securely encrypting messages and decrypting them with a provided passphrase. Use these commands to ensure message confidentiality and safe data transmission."` } +// CreateCryptCommand initializes a new cobra.Command, providing sub-commands for encrypting and decrypting messages. func CreateCryptCommand() *cobra.Command { instance := &cryptoCommand{ BaseCommand: types.BaseCommand{}, } - return commands.CreateTypedCommand(instance) + return commands.CreateTypedCommand(instance, commands.NonRunnable) } diff --git a/go.mod b/go.mod index 67c3345..be697f1 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,12 @@ require ( github.com/ProtonMail/go-crypto v1.1.0 // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect github.com/cloudflare/circl v1.3.7 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index 31e25e1..e9fb82e 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/ProtonMail/gopenpgp/v2 v2.8.0 h1:WvMv3CMcFsqKSM4/Qf8sf3tgyQkzDqQmoSE4 github.com/ProtonMail/gopenpgp/v2 v2.8.0/go.mod h1:qb2GUSnmA9ipBW5GVtCtEhkummSlqs2A8Ar3S0HBgSY= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -17,6 +18,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= diff --git a/pkg/charmer/command_line_application.go b/pkg/charmer/command_line_application.go index e09e731..9bc23c6 100644 --- a/pkg/charmer/command_line_application.go +++ b/pkg/charmer/command_line_application.go @@ -1,6 +1,7 @@ package charmer import ( + "github.com/matzefriedrich/cobra-extensions/pkg/commands" "github.com/matzefriedrich/cobra-extensions/pkg/types" "github.com/spf13/cobra" ) @@ -23,9 +24,12 @@ func NewRootCommand(name string, description string) *cobra.Command { // NewCommandLineApplication Creates a new CommandLineApplication instance. func NewCommandLineApplication(name string, description string) *CommandLineApplication { - return &CommandLineApplication{ - root: NewRootCommand(name, description), + rootCommand := NewRootCommand(name, description) + app := &CommandLineApplication{ + root: rootCommand, } + app.AddCommand(commands.NewMarkdownDocsCommand(rootCommand)) + return app } // Execute executes the root command of the CommandLineApplication. @@ -42,6 +46,7 @@ func (a *CommandLineApplication) AddCommand(c ...*cobra.Command) *CommandLineApp // AddGroupCommand Adds a sub-command to the root command and configures it using the provided setup function. func (a *CommandLineApplication) AddGroupCommand(c *cobra.Command, setup types.CommandsSetupFunc) *CommandLineApplication { a.root.AddCommand(c) + if setup != nil { wrapper := newCommandSetup(c) setup(wrapper) diff --git a/pkg/commands/markdown_command.go b/pkg/commands/markdown_command.go new file mode 100644 index 0000000..7ea6226 --- /dev/null +++ b/pkg/commands/markdown_command.go @@ -0,0 +1,73 @@ +package commands + +import ( + "github.com/matzefriedrich/cobra-extensions/internal/utils" + "github.com/matzefriedrich/cobra-extensions/pkg/types" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + "log" + "os" + "path/filepath" +) + +type markdownDocsCommand struct { + use types.CommandName `flag:"markdown" short:"Exports Markdown documentation to the specified folder" description:"Exports Markdown documentation to the specified folder"` + OutputFolderPath string `flag:"output" usage:"The output folder for markdown documentation" default:"."` + root *cobra.Command +} + +func (m *markdownDocsCommand) Execute() { + + outputPath, _ := filepath.Abs(m.OutputFolderPath) + + ext := filepath.Ext(outputPath) + switch ext { + case ".md", ".markdown": + m.generateSingleMarkdownFile(outputPath) + default: + err := doc.GenMarkdownTree(m.root, outputPath) + if err != nil { + log.Fatalf("Error generating markdown documentation: %v", err) + } + } + +} + +func (m *markdownDocsCommand) generateSingleMarkdownFile(outputPath string) { + + writer, _ := os.OpenFile(outputPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + defer func(writer *os.File) { + _ = writer.Close() + }(writer) + + stack := utils.MakeStack[*cobra.Command]() + stack.Push(m.root) + + for stack.Any() { + next := stack.Pop() + for _, c := range next.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + stack.Push(c) + } + + _ = doc.GenMarkdownCustom(next, writer, func(s string) string { + return s + }) + + _, _ = writer.WriteString("\n") + } + + _ = writer.Sync() +} + +var _ types.TypedCommand = (*markdownDocsCommand)(nil) + +// NewMarkdownDocsCommand creates a new Cobra command for exporting Markdown documentation to a specified folder. +func NewMarkdownDocsCommand(root *cobra.Command) *cobra.Command { + instance := &markdownDocsCommand{ + root: root, + } + return CreateTypedCommand(instance) +} diff --git a/pkg/commands/markdown_command_test.go b/pkg/commands/markdown_command_test.go new file mode 100644 index 0000000..0beac71 --- /dev/null +++ b/pkg/commands/markdown_command_test.go @@ -0,0 +1,41 @@ +package commands + +import ( + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func Test_MarkdownCommand_Execute_renders_multiple_files_if_folder_path_specified(t *testing.T) { + + // Arrange + app := &cobra.Command{} + sut := NewMarkdownDocsCommand(app) + + app.AddCommand(sut) + app.SetArgs([]string{"markdown", "--output", os.TempDir()}) + + // Act + err := app.Execute() + + // Assert + assert.NoError(t, err) +} + +func Test_MarkdownCommand_Execute_renders_single_file_if_markdown_file_path_specified(t *testing.T) { + + // Arrange + app := &cobra.Command{} + sut := NewMarkdownDocsCommand(app) + + app.AddCommand(sut) + app.SetArgs([]string{"markdown", "--output", filepath.Join(os.TempDir(), "test.md")}) + + // Act + err := app.Execute() + + // Assert + assert.NoError(t, err) +} diff --git a/pkg/commands/typed_command.go b/pkg/commands/typed_command.go index 7d52c52..7e024ab 100644 --- a/pkg/commands/typed_command.go +++ b/pkg/commands/typed_command.go @@ -12,6 +12,8 @@ type commandContextValue struct { descriptor types.CommandDescriptor } +type CommandOption func(cmd *cobra.Command) + // run Binds argument and flag values and executes the command. func (c *commandContextValue) run(target *cobra.Command, args ...string) { c.descriptor.UnmarshalFlagValues(target) @@ -20,7 +22,7 @@ func (c *commandContextValue) run(target *cobra.Command, args ...string) { } // CreateTypedCommand Creates a new typed command from the given handler instance. -func CreateTypedCommand[T types.TypedCommand](instance T) *cobra.Command { +func CreateTypedCommand[T types.TypedCommand](instance T, options ...func() CommandOption) *cobra.Command { reflector := reflection.NewCommandReflector[T]() desc := reflector.ReflectCommandDescriptor(instance) @@ -28,12 +30,18 @@ func CreateTypedCommand[T types.TypedCommand](instance T) *cobra.Command { commandKey := desc.Key() cmd := &cobra.Command{ + DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { v := cmd.Context().Value(commandKey).(*commandContextValue) v.run(cmd, args...) }, } + for _, option := range options { + f := option() + f(cmd) + } + desc.BindArguments(cmd) desc.BindFlags(cmd) @@ -47,3 +55,11 @@ func CreateTypedCommand[T types.TypedCommand](instance T) *cobra.Command { return cmd } + +// NonRunnable disables the Run and RunE functions of a Cobra command, effectively making the command non-runnable. +func NonRunnable() CommandOption { + return func(c *cobra.Command) { + c.Run = nil + c.RunE = nil + } +}