// Copyright 2023 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // If we decide to employ this auto generation of markdown documentation for // amtool and alertmanager, this package could potentially be moved to // prometheus/common. However, it is crucial to note that this functionality is // tailored specifically to the way in which the Prometheus documentation is // rendered, and should be avoided for use by third-party users. package documentcli import ( "bytes" "fmt" "io" "reflect" "strings" "github.com/alecthomas/kingpin/v2" "github.com/grafana/regexp" ) // GenerateMarkdown generates the markdown documentation for an application from // its kingpin ApplicationModel. func GenerateMarkdown(model *kingpin.ApplicationModel, writer io.Writer) error { h := header(model.Name, model.Help) if _, err := writer.Write(h); err != nil { return err } if err := writeFlagTable(writer, 0, model.FlagGroupModel); err != nil { return err } if err := writeArgTable(writer, 0, model.ArgGroupModel); err != nil { return err } if err := writeCmdTable(writer, model.CmdGroupModel); err != nil { return err } return writeSubcommands(writer, 1, model.Name, model.CmdGroupModel.Commands) } func header(title, help string) []byte { return []byte(fmt.Sprintf(`--- title: %s --- # %s %s `, title, title, help)) } func createFlagRow(flag *kingpin.FlagModel) []string { defaultVal := "" if len(flag.Default) > 0 && len(flag.Default[0]) > 0 { defaultVal = fmt.Sprintf("`%s`", flag.Default[0]) } name := fmt.Sprintf(`--%s`, flag.Name) if flag.Short != '\x00' { name = fmt.Sprintf(`-%c, --%s`, flag.Short, flag.Name) } valueType := reflect.TypeOf(flag.Value) if valueType.Kind() == reflect.Ptr { valueType = valueType.Elem() } if valueType.Kind() == reflect.Struct { if _, found := valueType.FieldByName("slice"); found { name = fmt.Sprintf(`%s ...`, name) } } return []string{name, strings.ReplaceAll(flag.Help, "|", `\|`), defaultVal} } func writeFlagTable(writer io.Writer, level int, fgm *kingpin.FlagGroupModel) error { if fgm == nil || len(fgm.Flags) == 0 { return nil } rows := [][]string{ {"Flag", "Description", "Default"}, } for _, flag := range fgm.Flags { if !flag.Hidden { row := createFlagRow(flag) rows = append(rows, row) } } return writeTable(writer, rows, fmt.Sprintf("%s Flags", strings.Repeat("#", level+2))) } func createArgRow(arg *kingpin.ArgModel) []string { defaultVal := "" if len(arg.Default) > 0 { defaultVal = fmt.Sprintf("`%s`", arg.Default[0]) } required := "" if arg.Required { required = "Yes" } return []string{arg.Name, arg.Help, defaultVal, required} } func writeArgTable(writer io.Writer, level int, agm *kingpin.ArgGroupModel) error { if agm == nil || len(agm.Args) == 0 { return nil } rows := [][]string{ {"Argument", "Description", "Default", "Required"}, } for _, arg := range agm.Args { row := createArgRow(arg) rows = append(rows, row) } return writeTable(writer, rows, fmt.Sprintf("%s Arguments", strings.Repeat("#", level+2))) } func createCmdRow(cmd *kingpin.CmdModel) []string { if cmd.Hidden { return nil } return []string{cmd.FullCommand, cmd.Help} } func writeCmdTable(writer io.Writer, cgm *kingpin.CmdGroupModel) error { if cgm == nil || len(cgm.Commands) == 0 { return nil } rows := [][]string{ {"Command", "Description"}, } for _, cmd := range cgm.Commands { row := createCmdRow(cmd) if row != nil { rows = append(rows, row) } } return writeTable(writer, rows, "## Commands") } func writeTable(writer io.Writer, data [][]string, header string) error { if len(data) < 2 { return nil } buf := bytes.NewBuffer(nil) buf.WriteString(fmt.Sprintf("\n\n%s\n\n", header)) columnsToRender := determineColumnsToRender(data) headers := data[0] buf.WriteString("|") for _, j := range columnsToRender { buf.WriteString(fmt.Sprintf(" %s |", headers[j])) } buf.WriteString("\n") buf.WriteString("|") for range columnsToRender { buf.WriteString(" --- |") } buf.WriteString("\n") for i := 1; i < len(data); i++ { row := data[i] buf.WriteString("|") for _, j := range columnsToRender { buf.WriteString(fmt.Sprintf(" %s |", row[j])) } buf.WriteString("\n") } if _, err := writer.Write(buf.Bytes()); err != nil { return err } if _, err := writer.Write([]byte("\n\n")); err != nil { return err } return nil } func determineColumnsToRender(data [][]string) []int { columnsToRender := []int{} if len(data) == 0 { return columnsToRender } for j := 0; j < len(data[0]); j++ { renderColumn := false for i := 1; i < len(data); i++ { if data[i][j] != "" { renderColumn = true break } } if renderColumn { columnsToRender = append(columnsToRender, j) } } return columnsToRender } func writeSubcommands(writer io.Writer, level int, modelName string, commands []*kingpin.CmdModel) error { level++ if level > 4 { level = 4 } for _, cmd := range commands { if cmd.Hidden { continue } help := cmd.Help if cmd.HelpLong != "" { help = cmd.HelpLong } help = formatHyphenatedWords(help) if _, err := writer.Write([]byte(fmt.Sprintf("\n\n%s `%s %s`\n\n%s\n\n", strings.Repeat("#", level+1), modelName, cmd.FullCommand, help))); err != nil { return err } if err := writeFlagTable(writer, level, cmd.FlagGroupModel); err != nil { return err } if err := writeArgTable(writer, level, cmd.ArgGroupModel); err != nil { return err } if cmd.CmdGroupModel != nil && len(cmd.CmdGroupModel.Commands) > 0 { if err := writeSubcommands(writer, level+1, modelName, cmd.CmdGroupModel.Commands); err != nil { return err } } } return nil } func formatHyphenatedWords(input string) string { hyphenRegex := regexp.MustCompile(`\B--\w+\b`) replacer := func(s string) string { return fmt.Sprintf("`%s`", s) } return hyphenRegex.ReplaceAllStringFunc(input, replacer) }