├── .github ├── FUNDING.yml └── workflows │ └── build.yaml ├── AUTHORS.md ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── _example ├── .gitignore ├── _manpage.md ├── _markdown.md └── example.go ├── cobradoc.go ├── format.go ├── format_markdown.tmpl ├── format_troff.tmpl ├── generate.go ├── go.mod └── go.sum /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: gavv 2 | liberapay: gavv 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: 8 | - main 9 | tags: 10 | - v* 11 | 12 | workflow_dispatch: 13 | 14 | schedule: 15 | - cron: '0 0 * * 1' 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | go: [1.16, 1.x] 24 | 25 | name: Go ${{ matrix.go }} 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Install Go 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version: ${{ matrix.go }} 34 | 35 | - name: Build 36 | run: make build 37 | 38 | - name: Example 39 | run: make example 40 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | cobradoc authors, ordered by first contribution: 4 | 5 | 6 | 7 | 1. Victor Gaydov `gavv` 8 | 2. Kyle Huggins `hugginsio` 9 | 10 | 11 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.2.0][v1.2.0] - 17 May 2025 4 | 5 | * Add `Options.HideHelp` that hides `help` command and `--help` option ([gh-2][gh-2]) 6 | 7 | [v1.2.0]: https://github.com/gavv/cobradoc/releases/tag/v1.2.0 8 | 9 | [gh-2]: https://github.com/gavv/cobradoc/issues/2 10 | 11 | ## [v1.1.0][v1.1.0] - 28 Jan 2023 12 | 13 | * Add `Options.Language` and use it for text transformations ([gh-1][gh-1]) 14 | * Fix markdown formatting ([gh-1][gh-1]) 15 | 16 | [v1.1.0]: https://github.com/gavv/cobradoc/releases/tag/v1.1.0 17 | 18 | [gh-1]: https://github.com/gavv/cobradoc/issues/1 19 | 20 | ## [v1.0.0][v1.0.0] - 28 Jan 2023 21 | 22 | * Initial release 23 | 24 | [v1.0.0]: https://github.com/gavv/cobradoc/releases/tag/v1.0.0 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Victor Gaydov and contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | 3 | all: tidy build 4 | 5 | build: 6 | go build . 7 | cd _example && go build -o example 8 | 9 | example: build 10 | ./_example/example markdown > _example/_markdown.md 11 | ( echo '```' ; COLUMNS=90 man <(./_example/example man) ; echo '```' ) > _example/_manpage.md 12 | 13 | tidy: 14 | go mod tidy -v 15 | cd _example && go mod tidy -v 16 | 17 | fmt: 18 | gofmt -s -w . ./_example 19 | 20 | md: 21 | md-authors --format modern --append AUTHORS.md 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cobradoc [![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/gavv/cobradoc) [![Build](https://github.com/gavv/cobradoc/workflows/build/badge.svg)](https://github.com/gavv/cobradoc/actions) [![GitHub release](https://img.shields.io/github/release/gavv/cobradoc.svg)](https://github.com/gavv/cobradoc/releases) 2 | 3 | Alternative documentation generator for golang [Cobra](https://github.com/spf13/cobra). 4 | 5 | Highlights 6 | ---------- 7 | 8 | * Supports markdown and manpage (troff) formats 9 | * Supports command groups 10 | * Generates single page for the whole command tree 11 | 12 | Reference 13 | --------- 14 | 15 | API reference is available on [pkg.go.dev](https://pkg.go.dev/github.com/gavv/cobradoc#section-documentation). 16 | 17 | Example 18 | ------- 19 | 20 | [_example](_example) directory demonstrates usage of this package. 21 | 22 | It contains: 23 | 24 | * [example cobra tool](_example/example.go) 25 | * [generated manual page](_example/_manpage.md) 26 | * [generated markdown](_example/_markdown.md) 27 | 28 | Usage 29 | ----- 30 | 31 | Generate markdown page: 32 | 33 | ```go 34 | import "github.com/gavv/cobradoc" 35 | 36 | err := cobradoc.WriteDocument(os.Stdout, rootCmd, cobradoc.Markdown, cobradoc.Options{ 37 | Name: "my-tool", 38 | Header: "My page header", 39 | ShortDescription: "My tool description", 40 | }) 41 | if err != nil { 42 | panic(err) 43 | } 44 | ``` 45 | 46 | Generate manual page: 47 | 48 | ```go 49 | import "github.com/gavv/cobradoc" 50 | 51 | err := cobradoc.WriteDocument(os.Stdout, rootCmd, cobradoc.Troff, cobradoc.Options{ 52 | Name: "my-tool", 53 | Header: "My page header", 54 | ShortDescription: "My tool description", 55 | ExtraSections: []cobradoc.ExtraSection{ 56 | { 57 | Title: cobradoc.BUGS, 58 | Text: "Please report bugs via GitHub", 59 | }, 60 | }, 61 | }) 62 | if err != nil { 63 | panic(err) 64 | } 65 | ``` 66 | 67 | Credits 68 | ------- 69 | 70 | This package is inspired by [cobraman](https://github.com/rayjohnson/cobraman) by Ray Johnson, but uses single-page approach and adds support for command groups. 71 | 72 | ## History 73 | 74 | You can find changelog here: [changelog](CHANGES.md). 75 | 76 | Authors 77 | ------- 78 | 79 | See [here](AUTHORS.md). 80 | 81 | License 82 | ------- 83 | 84 | [MIT](LICENSE) 85 | -------------------------------------------------------------------------------- /_example/.gitignore: -------------------------------------------------------------------------------- 1 | /example 2 | -------------------------------------------------------------------------------- /_example/_manpage.md: -------------------------------------------------------------------------------- 1 | ``` 2 | EXAMPLE(1) Example Manual EXAMPLE(1) 3 | 4 | NAME 5 | example - Example command 6 | 7 | SYNOPSIS 8 | example [command] [global flags] [command flags] 9 | 10 | DESCRIPTION 11 | Example command using cobra and cobradoc. 12 | 13 | Global flags: 14 | 15 | -v, --value=string 16 | string value 17 | 18 | --flag boolean flag 19 | 20 | MAIN COMMANDS 21 | example hello [flags] 22 | Say hello 23 | 24 | Command flags: 25 | 26 | -c, --count=1 27 | how many times to greet? 28 | 29 | -h, --help 30 | help for hello 31 | 32 | example bye [flags] 33 | Say goodbye 34 | 35 | Command flags: 36 | 37 | -n, --name=John 38 | who got the goodbye? 39 | 40 | -h, --help 41 | help for bye 42 | 43 | DOCUMENTATION COMMANDS 44 | example man [flags] 45 | Generate manual page 46 | 47 | Command flags: 48 | 49 | -h, --help 50 | help for man 51 | 52 | example markdown [flags] 53 | Generate markdown page 54 | 55 | Command flags: 56 | 57 | -h, --help 58 | help for markdown 59 | 60 | ADDITIONAL COMMANDS 61 | example help [command] [flags] 62 | Help provides help for any command in the application. Simply type exam‐ 63 | ple help [path to command] for full details. 64 | 65 | Command flags: 66 | 67 | -h, --help 68 | help for help 69 | 70 | REPORTING BUGS 71 | Please report bugs at https://github.com/gavv/cobradoc 72 | 73 | Example Manual May 2025 EXAMPLE(1) 74 | ``` 75 | -------------------------------------------------------------------------------- /_example/_markdown.md: -------------------------------------------------------------------------------- 1 | # Example Manual 2 | 3 | Example command using cobra and cobradoc. 4 | 5 | ```text 6 | example [command] [global flags] [command flags] 7 | ``` 8 | 9 | ### Global Flags 10 | 11 | ```text 12 | -v, --value string string value 13 | --flag boolean flag 14 | ``` 15 | 16 | ### Main Commands 17 | 18 | * [example hello](#example-hello) 19 | * [example bye](#example-bye) 20 | 21 | ### Documentation Commands 22 | 23 | * [example man](#example-man) 24 | * [example markdown](#example-markdown) 25 | 26 | ### Additional Commands 27 | 28 | * [example help](#example-help) 29 | 30 | # Main Commands 31 | 32 | ## `example hello` 33 | 34 | Say hello 35 | 36 | ```text 37 | example hello [flags] 38 | ``` 39 | 40 | ### Command Flags 41 | 42 | ```text 43 | -c, --count int how many times to greet? (default 1) 44 | -h, --help help for hello 45 | ``` 46 | 47 | ## `example bye` 48 | 49 | Say goodbye 50 | 51 | ```text 52 | example bye [flags] 53 | ``` 54 | 55 | ### Command Flags 56 | 57 | ```text 58 | -n, --name string who got the goodbye? (default "John") 59 | -h, --help help for bye 60 | ``` 61 | 62 | # Documentation Commands 63 | 64 | ## `example man` 65 | 66 | Generate manual page 67 | 68 | ```text 69 | example man [flags] 70 | ``` 71 | 72 | ### Command Flags 73 | 74 | ```text 75 | -h, --help help for man 76 | ``` 77 | 78 | ## `example markdown` 79 | 80 | Generate markdown page 81 | 82 | ```text 83 | example markdown [flags] 84 | ``` 85 | 86 | ### Command Flags 87 | 88 | ```text 89 | -h, --help help for markdown 90 | ``` 91 | 92 | # Additional Commands 93 | 94 | ## `example help` 95 | 96 | Help provides help for any command in the application. 97 | Simply type example help [path to command] for full details. 98 | 99 | ```text 100 | example help [command] [flags] 101 | ``` 102 | 103 | ### Command Flags 104 | 105 | ```text 106 | -h, --help help for help 107 | ``` 108 | -------------------------------------------------------------------------------- /_example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/gavv/cobradoc" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | const ( 12 | groupMain = "main" 13 | groupDocs = "docs" 14 | ) 15 | 16 | var rootCmd = &cobra.Command{ 17 | Use: "example", 18 | Short: "Example command", 19 | CompletionOptions: cobra.CompletionOptions{ 20 | DisableDefaultCmd: true, 21 | }, 22 | } 23 | 24 | var helloCmd = &cobra.Command{ 25 | GroupID: groupMain, 26 | Use: "hello", 27 | Short: "Say hello", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | count, _ := cmd.Flags().GetInt("count") 30 | for i := 0; i < count; i++ { 31 | fmt.Println("hello!") 32 | } 33 | }, 34 | } 35 | 36 | var byeCmd = &cobra.Command{ 37 | GroupID: groupMain, 38 | Use: "bye", 39 | Short: "Say goodbye", 40 | Run: func(cmd *cobra.Command, args []string) { 41 | name, _ := cmd.Flags().GetString("name") 42 | fmt.Printf("bye, %s!\n", name) 43 | }, 44 | } 45 | 46 | var manCmd = &cobra.Command{ 47 | GroupID: groupDocs, 48 | Use: "man", 49 | Short: "Generate manual page", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | cobradoc.WriteDocument(os.Stdout, rootCmd, cobradoc.Troff, cobradoc.Options{ 52 | LongDescription: "Example command using cobra and cobradoc.", 53 | ExtraSections: []cobradoc.ExtraSection{ 54 | { 55 | Title: cobradoc.BUGS, 56 | Text: "Please report bugs at https://github.com/gavv/cobradoc", 57 | }, 58 | }, 59 | }) 60 | }, 61 | } 62 | 63 | var markdownCmd = &cobra.Command{ 64 | GroupID: groupDocs, 65 | Use: "markdown", 66 | Short: "Generate markdown page", 67 | Run: func(cmd *cobra.Command, args []string) { 68 | cobradoc.WriteDocument(os.Stdout, rootCmd, cobradoc.Markdown, cobradoc.Options{ 69 | LongDescription: "Example command using cobra and cobradoc.", 70 | }) 71 | }, 72 | } 73 | 74 | func init() { 75 | cobra.EnableCommandSorting = false 76 | 77 | helloCmd.Flags().IntP("count", "c", 1, "how many times to greet?") 78 | helloCmd.Flags().SortFlags = false 79 | helloCmd.InheritedFlags().SortFlags = false 80 | 81 | byeCmd.PersistentFlags().StringP("name", "n", "John", "who got the goodbye?") 82 | byeCmd.Flags().SortFlags = false 83 | byeCmd.InheritedFlags().SortFlags = false 84 | 85 | rootCmd.PersistentFlags().StringP("value", "v", "", "string value") 86 | rootCmd.PersistentFlags().Bool("flag", false, "boolean flag") 87 | rootCmd.PersistentFlags().SortFlags = false 88 | 89 | rootCmd.AddGroup(&cobra.Group{ 90 | Title: "Main Commands", 91 | ID: groupMain, 92 | }) 93 | 94 | rootCmd.AddCommand(helloCmd) 95 | rootCmd.AddCommand(byeCmd) 96 | 97 | rootCmd.AddGroup(&cobra.Group{ 98 | Title: "Documentation Commands", 99 | ID: groupDocs, 100 | }) 101 | 102 | rootCmd.AddCommand(manCmd) 103 | rootCmd.AddCommand(markdownCmd) 104 | } 105 | 106 | func main() { 107 | rootCmd.Execute() 108 | } 109 | -------------------------------------------------------------------------------- /cobradoc.go: -------------------------------------------------------------------------------- 1 | // Package cobradoc implements alternative documentation generator for cobra. 2 | package cobradoc 3 | 4 | import ( 5 | "bytes" 6 | "io" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // Generation options 12 | // All fields have reasonable defaults 13 | type Options struct { 14 | // Page section number (defaults to "1") 15 | // In man page, defines manual page section number 16 | // In markdown, ignored 17 | SectionNumber string 18 | 19 | // Name of the tool (defaults to cmd.Name()) 20 | // Used in numerous places 21 | Name string 22 | 23 | // Creation date (defaults to current month and year) 24 | // In man page, located at the center of the bottom line 25 | // In markdown, ignored 26 | Date string 27 | 28 | // BCP 47 language for converting the tool name to title case (defaults to "en") 29 | Language string 30 | 31 | // Page header (defaults to "{Name} Manual") 32 | // In man page, located at the center of the top line 33 | // In markdown, defines page header 34 | Header string 35 | 36 | // Page footer (defaults to "{Name} Manual") 37 | // In man page, located at the left corner of the bottom line 38 | // In markdown, ignored 39 | Footer string 40 | 41 | // Short description of the tool (defaults to cmd.Short) 42 | // In man page, located in NAME section 43 | // In markdown, located in the first section, used if LongDescription is unset 44 | ShortDescription string 45 | 46 | // Long description of the tool (defaults to cmd.Long) 47 | // In man page, located in DESCRIPTION section 48 | // In markdown, located in the first section 49 | LongDescription string 50 | 51 | // Array of additional sections (optional) 52 | // Sections are added to the end of the document 53 | ExtraSections []ExtraSection 54 | 55 | // Hide help command and --help flags from generated document 56 | HideHelp bool 57 | } 58 | 59 | // Extra section contents 60 | type ExtraSection struct { 61 | // Section title 62 | Title string 63 | 64 | // Section text 65 | Text string 66 | } 67 | 68 | // Common titles for extra sections 69 | // Section title can be any string, however these ones are used frequently 70 | const ( 71 | EXAMPLES = "Examples" 72 | FILES = "Files" 73 | ENVIRONMENT = "Environment" 74 | BUGS = "Reporting Bugs" 75 | AUTHORS = "Authors" 76 | COPYRIGHT = "Copyright" 77 | LICENSE = "License" 78 | HISTORY = "History" 79 | SEEALSO = "See Also" 80 | NOTES = "Notes" 81 | ) 82 | 83 | // Output format 84 | type Format int 85 | 86 | const ( 87 | // Troff format (for manual page) 88 | Troff Format = iota 89 | 90 | // Markdown format 91 | Markdown Format = iota 92 | ) 93 | 94 | // Generate single documentation page for given command tree 95 | func GetDocument(cmd *cobra.Command, fmt Format, opts Options) (string, error) { 96 | var b bytes.Buffer 97 | 98 | if err := generate(cmd, fmt, opts, &b); err != nil { 99 | return "", err 100 | } 101 | 102 | return b.String(), nil 103 | } 104 | 105 | // Generate single documentation page for given command tree 106 | func WriteDocument(w io.Writer, cmd *cobra.Command, fmt Format, opts Options) error { 107 | var b bytes.Buffer 108 | 109 | if err := generate(cmd, fmt, opts, &b); err != nil { 110 | return err 111 | } 112 | 113 | // send to output writer only if there are no errors 114 | if _, err := io.Copy(w, &b); err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package cobradoc 2 | 3 | import ( 4 | _ "embed" 5 | "errors" 6 | "io" 7 | "regexp" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | type formatInfo struct { 13 | Options 14 | 15 | GlobalFlags []flagInfo 16 | GlobalFlagsBlock string 17 | 18 | Groups []groupInfo 19 | } 20 | 21 | type groupInfo struct { 22 | Title string 23 | Commands []commandInfo 24 | } 25 | 26 | type commandInfo struct { 27 | Path string 28 | Usage string 29 | Description string 30 | Flags []flagInfo 31 | FlagsBlock string 32 | } 33 | 34 | type flagInfo struct { 35 | Short string 36 | Long string 37 | DefaultValue string 38 | ValueIsOptional bool 39 | IsBool bool 40 | Type string 41 | Description string 42 | } 43 | 44 | var ( 45 | //go:embed format_troff.tmpl 46 | formatTroff string 47 | 48 | //go:embed format_markdown.tmpl 49 | formatMarkdown string 50 | ) 51 | 52 | var formatTemplates = map[Format]string{ 53 | Troff: formatTroff, 54 | Markdown: formatMarkdown, 55 | } 56 | 57 | var formatFuncs = map[Format]template.FuncMap{ 58 | Troff: { 59 | "upper": func(s string) string { 60 | return strings.ToUpper(s) 61 | }, 62 | "escape": func(s string) string { 63 | s = regexp.MustCompile(`\n+\n`).ReplaceAllString(s, "\n.PP\n") 64 | s = strings.NewReplacer( 65 | "-", "\\-", // 66 | "_", "\\_", // 67 | "&", "\\&", // 68 | "\\", "\\\\", // 69 | "~", "\\~", // 70 | ).Replace(s) 71 | return s 72 | }, 73 | }, 74 | Markdown: { 75 | "anchor": func(s string) string { 76 | return "#" + strings.ReplaceAll(s, " ", "-") 77 | }, 78 | }, 79 | } 80 | 81 | func format(fmt Format, fmtInfo formatInfo, w io.Writer) error { 82 | templateText, ok := formatTemplates[fmt] 83 | if !ok { 84 | return errors.New("invalid format") 85 | } 86 | 87 | templateFuncs, ok := formatFuncs[fmt] 88 | if !ok { 89 | return errors.New("invalid format") 90 | } 91 | 92 | t, err := template.New("cobradoc").Funcs(templateFuncs).Parse(templateText) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | err = t.Execute(w, fmtInfo) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /format_markdown.tmpl: -------------------------------------------------------------------------------- 1 | # {{ .Header }} 2 | 3 | {{- if .LongDescription }} 4 | 5 | {{ .LongDescription }} 6 | {{- else if .ShortDescription }} 7 | 8 | {{ .ShortDescription }} 9 | {{- end }} 10 | 11 | ```text 12 | {{ .Name }} [command] [global flags] [command flags] 13 | ``` 14 | 15 | {{- if .GlobalFlagsBlock }} 16 | 17 | ### Global Flags 18 | 19 | ```text 20 | {{ .GlobalFlagsBlock -}} 21 | ``` 22 | {{- end }} 23 | 24 | {{- range $group := .Groups }} 25 | 26 | ### {{ $group.Title }} 27 | {{ range $command := $group.Commands }} 28 | * [{{ $command.Path }}]({{ $command.Path | anchor }}) 29 | {{- end }} 30 | {{- end }} 31 | 32 | {{- range $group := .Groups }} 33 | 34 | # {{ $group.Title }} 35 | {{- range $command := $group.Commands }} 36 | 37 | ## `{{ $command.Path }}` 38 | 39 | {{ $command.Description }} 40 | 41 | ```text 42 | {{ $command.Usage }} 43 | ``` 44 | 45 | {{- if $command.FlagsBlock }} 46 | 47 | ### Command Flags 48 | 49 | ```text 50 | {{ $command.FlagsBlock -}} 51 | ``` 52 | {{- end }} 53 | {{- end }} 54 | {{- end }} 55 | {{- range $section := .ExtraSections }} 56 | 57 | # {{ $section.Title }} 58 | 59 | {{ $section.Text }} 60 | {{- end }} 61 | -------------------------------------------------------------------------------- /format_troff.tmpl: -------------------------------------------------------------------------------- 1 | .\" DO NOT EDIT! Generated by github.com/gavv/cobradoc 2 | .TH "{{ .Name | upper | escape }}" "{{ .SectionNumber | escape }}" "{{ .Date | escape }}" "{{ .Footer | escape }}" "{{ .Header | escape }}" 3 | .SH NAME 4 | {{ .Name | escape -}} 5 | {{- if .ShortDescription }} \- {{ .ShortDescription | escape }}{{ end }} 6 | .SH SYNOPSIS 7 | \fB{{ .Name | escape }}\fR [\fIcommand\fR] [\fIglobal flags\fR] [\fIcommand flags\fR] 8 | .SH DESCRIPTION 9 | .PP 10 | {{- if .LongDescription }} 11 | {{ .LongDescription | escape }} 12 | {{ end }} 13 | {{- if .GlobalFlags }} 14 | Global flags: 15 | {{ range $flag := .GlobalFlags }} 16 | .TP 17 | {{ if $flag.Short }}\fB{{ print "-" $flag.Short | escape }}\fP, {{ end -}} 18 | \fB{{ print "--" $flag.Long | escape -}}\fP 19 | {{- if not $flag.IsBool -}} 20 | {{- if $flag.ValueIsOptional -}} [ {{- end -}} 21 | =\fI 22 | {{- if $flag.DefaultValue -}} 23 | {{- $flag.DefaultValue | escape -}} 24 | {{- else -}} 25 | {{- $flag.Type | escape -}} 26 | {{- end -}} 27 | \fR 28 | {{- if $flag.ValueIsOptional -}} ] {{- end -}} 29 | {{- end }} 30 | {{ $flag.Description | escape }} 31 | {{- end }} 32 | {{- end }} 33 | {{- range $group := .Groups }} 34 | .SH {{ $group.Title | upper | escape }} 35 | {{- range $command := $group.Commands }} 36 | .TP 37 | \fI{{ $command.Usage | escape }}\fR 38 | {{ $command.Description | escape }} 39 | {{ if $command.Flags }} 40 | .RS 41 | 42 | Command flags: 43 | {{ range $flag := $command.Flags }} 44 | .TP 45 | {{ if $flag.Short }}\fB{{ print "-" $flag.Short | escape }}\fP, {{ end -}} 46 | \fB{{ print "--" $flag.Long | escape -}}\fP 47 | {{- if not $flag.IsBool -}} 48 | {{- if $flag.ValueIsOptional -}} [ {{- end -}} 49 | =\fI 50 | {{- if $flag.DefaultValue -}} 51 | {{- $flag.DefaultValue | escape -}} 52 | {{- else -}} 53 | {{- $flag.Type | escape -}} 54 | {{- end -}} 55 | \fR 56 | {{- if $flag.ValueIsOptional -}} ] {{- end -}} 57 | {{- end }} 58 | {{ $flag.Description | escape }} 59 | {{- end }} 60 | .RE 61 | {{- end }} 62 | {{ end }} 63 | {{- end }} 64 | {{- range $section := .ExtraSections }} 65 | .SH {{ $section.Title | upper | escape }} 66 | .PP 67 | {{ $section.Text | escape }} 68 | {{- end }} 69 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package cobradoc 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "time" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/pflag" 10 | "golang.org/x/text/cases" 11 | "golang.org/x/text/language" 12 | ) 13 | 14 | func generate(cmd *cobra.Command, fmt Format, opts Options, w io.Writer) error { 15 | prepareCommand(cmd, opts) 16 | 17 | prepareOptions(cmd, &opts) 18 | 19 | fmtInfo := makeFormatInfo(cmd, opts) 20 | 21 | return format(fmt, fmtInfo, w) 22 | } 23 | 24 | func prepareCommand(cmd *cobra.Command, opts Options) { 25 | cmd.InitDefaultHelpCmd() 26 | cmd.InitDefaultHelpFlag() 27 | 28 | if opts.HideHelp { 29 | if cmd.Name() == "help" { 30 | cmd.Hidden = true 31 | } 32 | if cmd.Flags().Lookup("help") != nil { 33 | // HasAvailableFlags and PrintDefaults will exclude --help 34 | cmd.Flags().MarkHidden("help") 35 | } 36 | } 37 | 38 | for _, subCmd := range cmd.Commands() { 39 | prepareCommand(subCmd, opts) 40 | } 41 | } 42 | 43 | func prepareOptions(cmd *cobra.Command, opts *Options) { 44 | if opts.SectionNumber == "" { 45 | opts.SectionNumber = "1" 46 | } 47 | 48 | if opts.Name == "" { 49 | opts.Name = cmd.Name() 50 | } 51 | 52 | if opts.Date == "" { 53 | opts.Date = time.Now().Format("Jan 2006") 54 | } 55 | 56 | if opts.Language == "" { 57 | opts.Language = "en" 58 | } 59 | 60 | languageTag, err := language.Parse(opts.Language) 61 | if err != nil { 62 | languageTag = language.English 63 | } 64 | 65 | if opts.Header == "" { 66 | opts.Header = cases.Title(languageTag).String(opts.Name) + " Manual" 67 | } 68 | 69 | if opts.Footer == "" { 70 | opts.Footer = cases.Title(languageTag).String(opts.Name) + " Manual" 71 | } 72 | 73 | if opts.ShortDescription == "" { 74 | opts.ShortDescription = cmd.Short 75 | } 76 | 77 | if opts.LongDescription == "" { 78 | opts.LongDescription = cmd.Long 79 | } 80 | } 81 | 82 | func makeFormatInfo(cmd *cobra.Command, opts Options) formatInfo { 83 | var fmtInfo formatInfo 84 | 85 | fmtInfo.Options = opts 86 | fmtInfo.Groups = makeGroupsInfo(cmd) 87 | 88 | globalFlags := cmd.PersistentFlags() 89 | 90 | if globalFlags.HasAvailableFlags() { 91 | fmtInfo.GlobalFlagsBlock = makeFlagsBlock(globalFlags) 92 | 93 | globalFlags.VisitAll(func(flag *pflag.Flag) { 94 | if flag.Hidden { 95 | return 96 | } 97 | fmtInfo.GlobalFlags = append(fmtInfo.GlobalFlags, makeFlagInfo(flag)) 98 | }) 99 | } 100 | 101 | return fmtInfo 102 | } 103 | 104 | func makeGroupsInfo(cmd *cobra.Command) []groupInfo { 105 | var grpInfoList []groupInfo 106 | 107 | for _, grp := range getGroups(cmd) { 108 | var grpInfo groupInfo 109 | 110 | grpInfo.Title = grp.Title 111 | 112 | for _, subCmd := range getCommands(cmd) { 113 | if subCmd.Hidden || len(subCmd.Deprecated) != 0 { 114 | continue 115 | } 116 | if len(cmd.Groups()) != 0 && subCmd.GroupID != grp.ID { 117 | continue 118 | } 119 | grpInfo.Commands = append(grpInfo.Commands, makeCommandInfo(subCmd)) 120 | } 121 | 122 | if len(grpInfo.Commands) == 0 { 123 | continue 124 | } 125 | 126 | grpInfoList = append(grpInfoList, grpInfo) 127 | } 128 | 129 | return grpInfoList 130 | } 131 | 132 | func makeCommandInfo(cmd *cobra.Command) commandInfo { 133 | var cmdInfo commandInfo 134 | 135 | cmdInfo.Path = cmd.CommandPath() 136 | cmdInfo.Usage = cmd.UseLine() 137 | 138 | if cmd.Long != "" { 139 | cmdInfo.Description = cmd.Long 140 | } else { 141 | cmdInfo.Description = cmd.Short 142 | } 143 | 144 | cmdFlags := cmd.NonInheritedFlags() 145 | 146 | if cmdFlags.HasAvailableFlags() { 147 | cmdInfo.FlagsBlock = makeFlagsBlock(cmdFlags) 148 | 149 | cmdFlags.VisitAll(func(flag *pflag.Flag) { 150 | if flag.Hidden { 151 | return 152 | } 153 | cmdInfo.Flags = append(cmdInfo.Flags, makeFlagInfo(flag)) 154 | }) 155 | } 156 | 157 | return cmdInfo 158 | } 159 | 160 | func makeFlagInfo(flag *pflag.Flag) flagInfo { 161 | return flagInfo{ 162 | Long: flag.Name, 163 | Short: flag.Shorthand, 164 | DefaultValue: flag.DefValue, 165 | ValueIsOptional: flag.NoOptDefVal != "", 166 | IsBool: flag.Value.Type() == "bool", 167 | Type: flag.Value.Type(), 168 | Description: flag.Usage, 169 | } 170 | } 171 | 172 | func makeFlagsBlock(flags *pflag.FlagSet) string { 173 | var buf bytes.Buffer 174 | 175 | flags.SetOutput(&buf) 176 | 177 | if flags.HasAvailableFlags() { 178 | flags.PrintDefaults() 179 | } 180 | 181 | return buf.String() 182 | } 183 | 184 | func getGroups(cmd *cobra.Command) []cobra.Group { 185 | var groups []cobra.Group 186 | 187 | if len(cmd.Groups()) == 0 { 188 | groups = []cobra.Group{ 189 | { 190 | ID: "", 191 | Title: "Commands", 192 | }, 193 | } 194 | } else { 195 | for _, grp := range cmd.Groups() { 196 | groups = append(groups, *grp) 197 | } 198 | groups = append(groups, cobra.Group{ 199 | ID: "", 200 | Title: "Additional Commands", 201 | }) 202 | } 203 | 204 | return groups 205 | } 206 | 207 | func getCommands(cmd *cobra.Command) []*cobra.Command { 208 | var commands []*cobra.Command 209 | 210 | for _, subCmd := range cmd.Commands() { 211 | commands = append(commands, subCmd) 212 | 213 | nestedCommands := getCommands(subCmd) 214 | if len(nestedCommands) != 0 { 215 | commands = append(commands, nestedCommands...) 216 | } 217 | } 218 | 219 | return commands 220 | } 221 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gavv/cobradoc 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/spf13/cobra v1.6.1 7 | github.com/spf13/pflag v1.0.5 8 | ) 9 | 10 | require golang.org/x/text v0.6.0 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 3 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 6 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 7 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 8 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 12 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 13 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 14 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 15 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 16 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 17 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 18 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 19 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 20 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 24 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 25 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 26 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 27 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 28 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 29 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 30 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 31 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 32 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 33 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | --------------------------------------------------------------------------------