From f6cd4588aff3897f895e0f0932dcd48f3dc33de5 Mon Sep 17 00:00:00 2001 From: Oleg Isakov Date: Wed, 28 Jan 2026 16:23:46 +0200 Subject: [PATCH 1/3] fix flags&subcommands format --- internal/docgen/generator.go | 44 +++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/internal/docgen/generator.go b/internal/docgen/generator.go index bb01297..61eb991 100644 --- a/internal/docgen/generator.go +++ b/internal/docgen/generator.go @@ -213,15 +213,16 @@ func (g *Generator) generateMarkdown(cmd *cobra.Command, extra *ExtraContent, co func (g *Generator) buildOptionsSection(cmd *cobra.Command) string { var buf bytes.Buffer - // Local flags - if cmd.LocalFlags().HasFlags() { - buf.WriteString("## Local Flags\n\n") + // Local flags (defined on this command) + if cmd.LocalFlags().HasAvailableFlags() { g.formatFlags(&buf, cmd.LocalFlags()) } - // Inherited flags (global) - if cmd.InheritedFlags().HasFlags() { - buf.WriteString("## Global Flags\n\n") + // Inherited flags (from parent commands) + if cmd.InheritedFlags().HasAvailableFlags() { + if buf.Len() > 0 { + buf.WriteString("\n") + } g.formatFlags(&buf, cmd.InheritedFlags()) } @@ -234,20 +235,31 @@ func (g *Generator) formatFlags(buf *bytes.Buffer, flags *pflag.FlagSet) { if flag.Hidden { return } - fmt.Fprintf(buf, "**--%s**", flag.Name) - if flag.Shorthand != "" { - fmt.Fprintf(buf, ", **-%s**", flag.Shorthand) - } + var flagTypeLong, flagTypeShort string if flag.Value.Type() != "bool" { - fmt.Fprintf(buf, " *%s*", flag.Value.Type()) + typeName := flag.Value.Type() + // Handle empty type or special cases + if typeName == "" || typeName == "[]" { + typeName = "value" + } + flagTypeLong = fmt.Sprintf("`=[<%s>]`", typeName) + flagTypeShort = fmt.Sprintf("`[<%s>]`", typeName) } - buf.WriteString("\n\n") + + // Write flag name and type + fmt.Fprintf(buf, "**--%s**%s", flag.Name, flagTypeLong) + if flag.Shorthand != "" { + fmt.Fprintf(buf, ", **-%s**%s", flag.Shorthand, flagTypeShort) + } + buf.WriteString("\n") + if flag.Usage != "" { - fmt.Fprintf(buf, " %s\n\n", flag.Usage) + fmt.Fprintf(buf, " %s", flag.Usage) } if flag.DefValue != "" && flag.DefValue != "false" && flag.DefValue != "[]" && flag.DefValue != "0" { - fmt.Fprintf(buf, " Default: `%s`\n\n", flag.DefValue) + fmt.Fprintf(buf, ". The default is `%s`.", flag.DefValue) } + buf.WriteString("\n\n") }) } @@ -259,9 +271,9 @@ func (g *Generator) buildSubCommandsSection(cmd *cobra.Command) string { if subCmd.Hidden || subCmd.Deprecated != "" { continue } - fmt.Fprintf(&buf, "**%s**\n\n", subCmd.Name()) + fmt.Fprintf(&buf, "**%s**\n", subCmd.Name()) if subCmd.Short != "" { - fmt.Fprintf(&buf, " %s\n\n", subCmd.Short) + fmt.Fprintf(&buf, " %s\n\n", subCmd.Short) } } From d95b3ad77d45d379c21e47606cad698c0197b965 Mon Sep 17 00:00:00 2001 From: Oleg Isakov Date: Wed, 28 Jan 2026 18:05:17 +0200 Subject: [PATCH 2/3] format fixes for docs generator --- cmd/root.go | 3 +- internal/docgen/generator.go | 57 ++++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 23d5df6..552fef9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,14 +39,13 @@ func NewRootCmd(version string) *cobra.Command { cmdContext := &base.CmdContext{} cmd := &cobra.Command{ - Use: "srvctl", + Use: "srvctl [command] [flags]", Short: "CLI tool for servers.com API", Long: `A command line interface for managing servers.com resources`, Version: version, PersistentPreRunE: base.InitCmdContext(cmdContext), SilenceUsage: true, } - // Global flags base.AddGlobalFlags(cmd) diff --git a/internal/docgen/generator.go b/internal/docgen/generator.go index 61eb991..3247c2c 100644 --- a/internal/docgen/generator.go +++ b/internal/docgen/generator.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "text/template" + "time" "github.com/cpuguy83/go-md2man/v2/md2man" "github.com/spf13/cobra" @@ -27,9 +28,10 @@ type ExtraContent struct { Examples string } -const docTemplate = `# NAME +const docTemplate = `% "{{.NameUpper}}" "1" "{{.Date}}" "" "" +# NAME -{{.Name}} - {{.Short}} +**{{.Name}}** - {{.Short}} # SYNOPSIS @@ -170,15 +172,14 @@ func (g *Generator) readExtraContent(commandPath string) (*ExtraContent, error) // generateMarkdown generates markdown documentation for a command func (g *Generator) generateMarkdown(cmd *cobra.Command, extra *ExtraContent, commandPath string) (string, error) { // Build description section - description := strings.TrimSpace(cmd.Long) - if description == "" { - description = cmd.Short - } + var description string if extra.Description != "" { - if description != "" { - description += "\n\n" + description = strings.TrimSpace(extra.Description) + } else { + description = strings.TrimSpace(cmd.Long) + if description == "" { + description = cmd.Short } - description += strings.TrimSpace(extra.Description) } // Build options section @@ -190,11 +191,19 @@ func (g *Generator) generateMarkdown(cmd *cobra.Command, extra *ExtraContent, co // Build examples section examples := strings.TrimSpace(extra.Examples) + now := time.Now() + curDate := now.Format("January 2006") + + // Format UseLine with command in bold + useLine := g.formatUseLine(cmd.UseLine()) + // Prepare data for template data := map[string]string{ "Name": commandPath, + "NameUpper": strings.ToUpper(commandPath), + "Date": curDate, "Short": cmd.Short, - "UseLine": cmd.UseLine(), + "UseLine": useLine, "Description": description, "Options": options, "SubCommands": subCommands, @@ -238,9 +247,9 @@ func (g *Generator) formatFlags(buf *bytes.Buffer, flags *pflag.FlagSet) { var flagTypeLong, flagTypeShort string if flag.Value.Type() != "bool" { typeName := flag.Value.Type() - // Handle empty type or special cases - if typeName == "" || typeName == "[]" { - typeName = "value" + // Simplify stringToX types + if strings.HasPrefix(typeName, "stringTo") { + typeName = "string" } flagTypeLong = fmt.Sprintf("`=[<%s>]`", typeName) flagTypeShort = fmt.Sprintf("`[<%s>]`", typeName) @@ -280,6 +289,28 @@ func (g *Generator) buildSubCommandsSection(cmd *cobra.Command) string { return strings.TrimSpace(buf.String()) } +// formatUseLine formats the UseLine by bolding only the command part +// Leaves arguments like , [flags], etc. unformatted +func (g *Generator) formatUseLine(useLine string) string { + parts := strings.Fields(useLine) + var result strings.Builder + + for i, part := range parts { + if i > 0 { + result.WriteString(" ") + } + + // Bold command parts, leave arguments/flags as-is + if strings.HasPrefix(part, "<") || strings.HasPrefix(part, "[") { + result.WriteString(part) + } else { + result.WriteString("**" + part + "**") + } + } + + return result.String() +} + // convertToMan converts markdown to man page format func (g *Generator) convertToMan(markdown string, outputPath string) error { manContent := md2man.Render([]byte(markdown)) From 71d9b2ab3dccd4627d065b2d8cd055d60b5c2b8f Mon Sep 17 00:00:00 2001 From: Oleg Isakov Date: Fri, 30 Jan 2026 11:24:45 +0200 Subject: [PATCH 3/3] add roff separator for generated docs --- internal/docgen/generator.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/docgen/generator.go b/internal/docgen/generator.go index 3247c2c..eac3d80 100644 --- a/internal/docgen/generator.go +++ b/internal/docgen/generator.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "text/template" "time" @@ -30,30 +31,24 @@ type ExtraContent struct { const docTemplate = `% "{{.NameUpper}}" "1" "{{.Date}}" "" "" # NAME - **{{.Name}}** - {{.Short}} # SYNOPSIS - {{.UseLine}} # DESCRIPTION - {{.Description}} # OPTIONS - {{.Options}} {{if .SubCommands}} # SUB COMMANDS - {{.SubCommands}} {{end}} {{if .Examples}} # EXAMPLES - {{.Examples}} {{end}} ` @@ -315,7 +310,11 @@ func (g *Generator) formatUseLine(useLine string) string { func (g *Generator) convertToMan(markdown string, outputPath string) error { manContent := md2man.Render([]byte(markdown)) - if err := os.WriteFile(outputPath, manContent, 0644); err != nil { + // Replace empty lines with a single dot (roff separator) + emptyLineRegex := regexp.MustCompile(`\n\n+`) + result := emptyLineRegex.ReplaceAllString(string(manContent), "\n.\n") + + if err := os.WriteFile(outputPath, []byte(result), 0644); err != nil { return fmt.Errorf("failed to write man page: %w", err) }