Skip to content

Commit

Permalink
Feature/markdown command (#12)
Browse files Browse the repository at this point in the history
* Adds Markdown documentation command
* Updates command descriptions and example app details
* Refactors command options to improve flexibility
* Adds tests for rendering markdown command output.
  • Loading branch information
matzefriedrich authored Nov 27, 2024
1 parent 4b66068 commit 7f29ab9
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 6 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion example/cmd/charmer/charmer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions example/cmd/simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -12,6 +13,7 @@ func main() {

app := charmer.NewRootCommand("simple-example", "")

app.AddCommand(builtin.NewMarkdownDocsCommand(app))
app.AddCommand(commands.CreateHelloCommand())

AddCryptCommands(app)
Expand Down
5 changes: 3 additions & 2 deletions example/commands/crypto_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
9 changes: 7 additions & 2 deletions pkg/charmer/command_line_application.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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.
Expand All @@ -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)
Expand Down
73 changes: 73 additions & 0 deletions pkg/commands/markdown_command.go
Original file line number Diff line number Diff line change
@@ -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)
}
41 changes: 41 additions & 0 deletions pkg/commands/markdown_command_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
18 changes: 17 additions & 1 deletion pkg/commands/typed_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -20,20 +22,26 @@ 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)

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)

Expand All @@ -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
}
}

0 comments on commit 7f29ab9

Please sign in to comment.