mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(docs): rework CLI docs (#6312)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||
# {{ .Name }}
|
||||
{{ if .Cmd.Long }}
|
||||
{{ .Cmd.Long }}
|
||||
{{ else }}
|
||||
{{ .Cmd.Short }}
|
||||
{{ end }}
|
||||
|
||||
{{- if .Cmd.Runnable}}
|
||||
## Usage
|
||||
```console
|
||||
{{.Cmd.UseLine}}
|
||||
```
|
||||
{{end}}
|
||||
|
||||
{{- if .Cmd.HasExample}}
|
||||
## Examples
|
||||
```console
|
||||
{{.Cmd.Example}}
|
||||
```
|
||||
{{end}}
|
||||
|
||||
{{- range $index, $cmd := .VisibleSubcommands }}
|
||||
{{- if eq $index 0 }}
|
||||
## Subcommands
|
||||
| Name | Purpose |
|
||||
| ---- | ----- |
|
||||
{{- end }}
|
||||
| [{{ $cmd.Name | wrapCode }}](./{{if $.AtRoot}}cli/{{end}}{{commandURI $cmd}}) | {{ $cmd.Short }} |
|
||||
{{- end}}
|
||||
{{ "" }}
|
||||
{{- range $index, $flag := .Flags }}
|
||||
{{- if eq $index 0 }}
|
||||
## Flags
|
||||
{{- end }}
|
||||
### --{{ $flag.Name }}{{ if $flag.Shorthand}}, -{{ $flag.Shorthand }}{{end}}
|
||||
{{ $flag.Usage | stripEnv | newLinesToBr }}
|
||||
<br/>
|
||||
| | |
|
||||
| --- | --- |
|
||||
{{- with $flag.Usage | parseEnv }}
|
||||
| Consumes | {{ . | wrapCode }} |
|
||||
{{- end }}
|
||||
{{- with $flag.DefValue }}
|
||||
| Default | {{" "}} {{- . | wrapCode }} |
|
||||
{{ "" }}
|
||||
{{ end }}
|
||||
{{ "" }}
|
||||
{{- end}}
|
||||
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/acarl005/stripansi"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/coder/coder/buildinfo"
|
||||
"github.com/coder/flog"
|
||||
)
|
||||
|
||||
//go:embed command.tpl
|
||||
var commandTemplateRaw string
|
||||
|
||||
var commandTemplate *template.Template
|
||||
|
||||
var envRegex = regexp.MustCompile(`Consumes (\$\w+).?$`)
|
||||
|
||||
func parseEnv(flagUsage string) string {
|
||||
flagUsage = stripansi.Strip(flagUsage)
|
||||
|
||||
ss := envRegex.FindStringSubmatch(flagUsage)
|
||||
if len(ss) == 0 {
|
||||
return ""
|
||||
}
|
||||
return ss[len(ss)-1]
|
||||
}
|
||||
|
||||
func stripEnv(flagUsage string) string {
|
||||
flagUsage = stripansi.Strip(flagUsage)
|
||||
ss := envRegex.FindStringSubmatch(flagUsage)
|
||||
if len(ss) == 0 {
|
||||
return flagUsage
|
||||
}
|
||||
return strings.TrimSpace(strings.ReplaceAll(flagUsage, ss[0], ""))
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandTemplate = template.Must(
|
||||
template.New("command.tpl").Funcs(template.FuncMap{
|
||||
"newLinesToBr": func(s string) string {
|
||||
return strings.ReplaceAll(s, "\n", "<br/>")
|
||||
},
|
||||
"wrapCode": func(s string) string {
|
||||
return fmt.Sprintf("<code>%s</code>", s)
|
||||
},
|
||||
"parseEnv": parseEnv,
|
||||
"stripEnv": stripEnv,
|
||||
"commandURI": func(cmd *cobra.Command) string {
|
||||
return strings.TrimSuffix(
|
||||
fmtDocFilename(cmd),
|
||||
".md",
|
||||
)
|
||||
},
|
||||
},
|
||||
).Parse(strings.TrimSpace(commandTemplateRaw)),
|
||||
)
|
||||
}
|
||||
|
||||
func writeCommand(w io.Writer, cmd *cobra.Command) error {
|
||||
var (
|
||||
flags []*pflag.Flag
|
||||
inheritedFlags []*pflag.Flag
|
||||
)
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Hidden {
|
||||
return
|
||||
}
|
||||
flags = append(flags, f)
|
||||
})
|
||||
cmd.InheritedFlags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Hidden {
|
||||
return
|
||||
}
|
||||
inheritedFlags = append(inheritedFlags, f)
|
||||
})
|
||||
var b strings.Builder
|
||||
err := commandTemplate.Execute(&b, map[string]any{
|
||||
"Name": fullCommandName(cmd),
|
||||
"Cmd": cmd,
|
||||
"Flags": flags,
|
||||
"InheritedFlags": inheritedFlags,
|
||||
"AtRoot": cmd.Parent() == nil,
|
||||
"VisibleSubcommands": func() []*cobra.Command {
|
||||
var scs []*cobra.Command
|
||||
for _, sub := range cmd.Commands() {
|
||||
if sub.Hidden {
|
||||
continue
|
||||
}
|
||||
scs = append(scs, sub)
|
||||
}
|
||||
return scs
|
||||
}(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := stripansi.Strip(b.String())
|
||||
|
||||
// Remove the version and its right space, since during this script running
|
||||
// there is no build info available
|
||||
content = strings.ReplaceAll(content, buildinfo.Version()+" ", "")
|
||||
|
||||
// Remove references to the current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content = strings.ReplaceAll(content, cwd, ".")
|
||||
|
||||
_, err = w.Write([]byte(content))
|
||||
return err
|
||||
}
|
||||
|
||||
func fullCommandName(cmd *cobra.Command) string {
|
||||
name := cmd.Name()
|
||||
if cmd.Parent() != nil {
|
||||
return fullCommandName(cmd.Parent()) + " " + name
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func fmtDocFilename(cmd *cobra.Command) string {
|
||||
fullName := fullCommandName(cmd)
|
||||
if fullName == "coder" {
|
||||
// Special case for index.
|
||||
return "../cli.md"
|
||||
}
|
||||
name := strings.ReplaceAll(fullName, " ", "_")
|
||||
return fmt.Sprintf("%s.md", name)
|
||||
}
|
||||
|
||||
func generateDocsTree(rootCmd *cobra.Command, basePath string) error {
|
||||
if rootCmd.Hidden {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write out root.
|
||||
fi, err := os.OpenFile(
|
||||
filepath.Join(basePath, fmtDocFilename(rootCmd)),
|
||||
os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fi.Close()
|
||||
|
||||
err = writeCommand(fi, rootCmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flog.Info("Generated docs for %q at %v", fullCommandName(rootCmd), fi.Name())
|
||||
|
||||
// Recursively generate docs.
|
||||
for _, subcommand := range rootCmd.Commands() {
|
||||
err = generateDocsTree(subcommand, basePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_parseEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
flagUsage string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"no env",
|
||||
args{"Perform a trial run with no changes made, showing a diff at the end."},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"env",
|
||||
args{`Specifies the path to an SSH config.
|
||||
Consumes $CODER_SSH_CONFIG_FILE`},
|
||||
"$CODER_SSH_CONFIG_FILE",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := parseEnv(tt.args.flagUsage); got != tt.want {
|
||||
t.Errorf("parseEnv() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
|
||||
"github.com/coder/coder/buildinfo"
|
||||
"github.com/coder/coder/cli"
|
||||
)
|
||||
|
||||
@@ -48,11 +42,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatal("Error on getting the current user: ", err)
|
||||
}
|
||||
|
||||
// Get the cmd CLI
|
||||
cmd := cli.Root(cli.AGPL())
|
||||
|
||||
@@ -64,18 +53,8 @@ func main() {
|
||||
markdownDocsDir := path.Join(basePath, "docs/cli")
|
||||
manifestFilepath := path.Join(basePath, "docs/manifest.json")
|
||||
|
||||
// Disables the "Auto generated by spf13/cobra" tag
|
||||
var disableAutoGenTag func(cmd *cobra.Command)
|
||||
disableAutoGenTag = func(cmd *cobra.Command) {
|
||||
for _, c := range cmd.Commands() {
|
||||
disableAutoGenTag(c)
|
||||
}
|
||||
cmd.DisableAutoGenTag = true
|
||||
}
|
||||
disableAutoGenTag(cmd)
|
||||
|
||||
// Generate markdown
|
||||
err = doc.GenMarkdownTree(cmd, markdownDocsDir)
|
||||
err := generateDocsTree(cmd, markdownDocsDir)
|
||||
if err != nil {
|
||||
log.Fatal("Error on generating CLI markdown docs: ", err)
|
||||
}
|
||||
@@ -96,38 +75,6 @@ func main() {
|
||||
Title: title,
|
||||
Path: "./cli/" + file.Name(),
|
||||
})
|
||||
|
||||
filepath := path.Join(markdownDocsDir, file.Name())
|
||||
openFile, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
log.Fatal("Error on open file at ", filepath, ": ", err)
|
||||
}
|
||||
content := string(openFile)
|
||||
|
||||
// Remove non printable strings from generated markdown
|
||||
// https://github.com/spf13/cobra/issues/1878
|
||||
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
|
||||
ansiRegex := regexp.MustCompile(ansi)
|
||||
content = ansiRegex.ReplaceAllString(content, "")
|
||||
|
||||
// Remove the version and its right space, since during this script running
|
||||
// there is no build info available
|
||||
content = strings.ReplaceAll(content, buildinfo.Version()+" ", "")
|
||||
|
||||
// Remove references to the current working directory
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal("Error on getting the current directory:", err)
|
||||
}
|
||||
content = strings.ReplaceAll(content, dir, "<current-directory>")
|
||||
|
||||
// Remove all absolute home paths.
|
||||
content = strings.ReplaceAll(content, u.HomeDir, "~")
|
||||
|
||||
err = os.WriteFile(filepath, []byte(content), 0o644) // #nosec
|
||||
if err != nil {
|
||||
log.Fatal("Error on save file at ", filepath, ": ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Read manifest
|
||||
|
||||
Reference in New Issue
Block a user