feat: convert entire CLI to clibase (#6491)

I'm sorry.
This commit is contained in:
Ammar Bandukwala
2023-03-23 17:42:20 -05:00
committed by GitHub
parent b71b8daa21
commit 2bd6d2908e
345 changed files with 9965 additions and 9082 deletions
+5 -1
View File
@@ -9,7 +9,11 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
cdroot
set +e
find . -regex ".*\.go" | grep -v "./enterprise" | grep -v ./scripts/auditdocgen/ --include="*.go" | xargs grep -n "github.com/coder/coder/enterprise"
find . -regex ".*\.go" |
grep -v "./enterprise" |
grep -v ./scripts/auditdocgen/ --include="*.go" |
grep -v ./scripts/clidocgen/ --include="*.go" |
xargs grep -n "github.com/coder/coder/enterprise"
# reverse the exit code because we want this script to fail if grep finds anything.
status=$?
set -e
+30 -23
View File
@@ -1,49 +1,56 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# {{ .Name }}
{{ if .Cmd.Long }}
{{ .Cmd.Long }}
{{ else }}
{{ .Cmd.Short }}
# {{ fullName . }}
{{ with .Short }}
{{ . }}
{{ end }}
{{- if .Cmd.Runnable}}
{{ with .Aliases }}
Aliases:
{{- range $index, $alias := . }}
* {{ $alias }}
{{- end }}
{{ end }}
{{- if .Use }}
## Usage
```console
{{.Cmd.UseLine}}
{{ .FullUsage }}
```
{{end}}
{{- if .Cmd.HasExample}}
## Examples
{{- if .Long}}
## Description
```console
{{.Cmd.Example}}
{{.Long}}
```
{{end}}
{{- range $index, $cmd := .VisibleSubcommands }}
{{- range $index, $cmd := visibleSubcommands . }}
{{- if eq $index 0 }}
## Subcommands
| Name | Purpose |
| ---- | ----- |
{{- end }}
| [{{ $cmd.Name | wrapCode }}](./{{if $.AtRoot}}cli/{{end}}{{commandURI $cmd}}) | {{ $cmd.Short }} |
| [{{ $cmd.Name | wrapCode }}](./{{if atRoot $}}cli/{{end}}{{commandURI $cmd}}) | {{ $cmd.Short }} |
{{- end}}
{{ "" }}
{{- range $index, $flag := .Flags }}
{{- range $index, $opt := visibleOptions . }}
{{- if eq $index 0 }}
## Flags
## Options
{{- end }}
### --{{ $flag.Name }}{{ if $flag.Shorthand}}, -{{ $flag.Shorthand }}{{end}}
{{ $flag.Usage | stripEnv | newLinesToBr }}
<br/>
| | |
| --- | --- |
{{- with $flag.Usage | parseEnv }}
| Consumes | {{ . | wrapCode }} |
### {{ with $opt.FlagShorthand}}-{{ . }}, {{end}}--{{ $opt.Flag }}
{{" "}}
{{ tableHeader }}
| Type | {{ $opt.Value.Type | wrapCode }} |
{{- with $opt.Env }}
| Environment | {{ (print "$" .) | wrapCode }} |
{{- end }}
{{- with $flag.DefValue }}
| Default | {{" "}} {{- . | wrapCode }} |
{{- with $opt.Default }}
| Default | {{- . | wrapCode }} |
{{ "" }}
{{ end }}
{{ "" }}
{{ $opt.Description | newLinesToBr }}
{{- end}}
+66 -93
View File
@@ -5,17 +5,15 @@ import (
"io"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"github.com/acarl005/stripansi"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
_ "embed"
"github.com/acarl005/stripansi"
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/cli/clibase"
"github.com/coder/flog"
)
@@ -24,84 +22,73 @@ 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{
"visibleSubcommands": func(cmd *clibase.Cmd) []*clibase.Cmd {
var visible []*clibase.Cmd
for _, sub := range cmd.Children {
if sub.Hidden {
continue
}
visible = append(visible, sub)
}
return visible
},
"visibleOptions": func(cmd *clibase.Cmd) []clibase.Option {
var visible []clibase.Option
for _, opt := range cmd.Options {
if opt.Hidden {
continue
}
visible = append(visible, opt)
}
return visible
},
"atRoot": func(cmd *clibase.Cmd) bool {
return cmd.FullName() == "coder"
},
"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 {
"commandURI": func(cmd *clibase.Cmd) string {
return strings.TrimSuffix(
fmtDocFilename(cmd),
".md",
)
},
"fullName": fullName,
"tableHeader": func() string {
return `| | |
| --- | --- |`
},
},
).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)
})
func fullName(cmd *clibase.Cmd) string {
if cmd.FullName() == "coder" {
return "coder"
}
return strings.TrimPrefix(cmd.FullName(), "coder ")
}
func fmtDocFilename(cmd *clibase.Cmd) string {
if cmd.FullName() == "coder" {
// Special case for index.
return "../cli.md"
}
name := strings.ReplaceAll(fullName(cmd), " ", "_")
return fmt.Sprintf("%s.md", name)
}
func writeCommand(w io.Writer, cmd *clibase.Cmd) error {
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
}(),
})
err := commandTemplate.Execute(&b, cmd)
if err != nil {
return err
}
@@ -118,41 +105,25 @@ func writeCommand(w io.Writer, cmd *cobra.Command) error {
}
content = strings.ReplaceAll(content, cwd, ".")
homedir, err := os.UserHomeDir()
if err != nil {
return err
}
content = strings.ReplaceAll(content, homedir, "~")
_, 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(cmd *cobra.Command, basePath string) error {
func genTree(dir string, cmd *clibase.Cmd, wroteLog map[string]*clibase.Cmd) error {
if cmd.Hidden {
return nil
}
if cmd.Name() == "server" {
// The server command is now managed by clibase and needs a new generator.
return nil
}
path := filepath.Join(dir, fmtDocFilename(cmd))
// Write out root.
fi, err := os.OpenFile(
filepath.Join(basePath, fmtDocFilename(cmd)),
path,
os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644,
)
if err != nil {
@@ -165,11 +136,13 @@ func generateDocsTree(cmd *cobra.Command, basePath string) error {
return err
}
flog.Info("Generated docs for %q at %v", fullCommandName(cmd), fi.Name())
// Recursively generate docs.
for _, subcommand := range cmd.Commands() {
err = generateDocsTree(subcommand, basePath)
flog.Successf(
"wrote\t%s",
fi.Name(),
)
wroteLog[path] = cmd
for _, sub := range cmd.Children {
err = genTree(dir, sub, wroteLog)
if err != nil {
return err
}
-40
View File
@@ -1,40 +0,0 @@
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)
}
})
}
}
+122 -56
View File
@@ -2,14 +2,17 @@ package main
import (
"encoding/json"
"log"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/enterprise/cli"
"github.com/coder/flog"
)
// route is an individual page object in the docs manifest.json.
type route struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
@@ -19,90 +22,153 @@ type route struct {
Children []route `json:"children,omitempty"`
}
// manifest describes the entire documentation index.
type manifest struct {
Versions []string `json:"versions,omitempty"`
Routes []route `json:"routes,omitempty"`
}
func main() {
func unsetCoderEnv() {
for _, env := range os.Environ() {
if strings.HasPrefix(env, "CODER_") {
split := strings.SplitN(env, "=", 2)
if err := os.Unsetenv(split[0]); err != nil {
log.Fatal("Unable to unset ", split[0], ": ", err)
panic(err)
}
}
}
for k, v := range map[string]string{
"CODER_CONFIG_DIR": "~/.config/coderv2",
"CODER_CACHE_DIRECTORY": "~/.cache/coder",
} {
if err := os.Setenv(k, v); err != nil {
log.Fatal("Unable to set default value for ", k, ": ", err)
}
func deleteEmptyDirs(dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
}
if !info.IsDir() {
return nil
}
ents, err := os.ReadDir(path)
if err != nil {
return err
}
if len(ents) == 0 {
flog.Infof("deleting empty dir\t %v", path)
err = os.Remove(path)
if err != nil {
return err
}
}
return nil
})
}
// Get the cmd CLI
cmd := cli.Root(cli.AGPL())
func main() {
unsetCoderEnv()
// Get paths
basePath := os.Getenv("BASE_PATH")
if basePath == "" {
log.Fatal("BASE_PATH should be defined")
}
markdownDocsDir := path.Join(basePath, "docs/cli")
manifestFilepath := path.Join(basePath, "docs/manifest.json")
// Generate markdown
err := generateDocsTree(cmd, markdownDocsDir)
workdir, err := os.Getwd()
if err != nil {
log.Fatal("Error on generating CLI markdown docs: ", err)
flog.Fatalf("getwd: %v", err)
}
root := (&cli.RootCmd{})
// wroteMap indexes file paths to commands.
wroteMap := make(map[string]*clibase.Cmd)
var (
docsDir = filepath.Join(workdir, "docs")
cliMarkdownDir = filepath.Join(docsDir, "cli")
)
cmd, err := root.Command(root.EnterpriseSubcommands())
if err != nil {
flog.Fatalf("creating command: %v", err)
}
err = genTree(
cliMarkdownDir,
cmd,
wroteMap,
)
if err != nil {
flog.Fatalf("generating markdowns: %v", err)
}
// Create CLI routes
var cliRoutes []route
files, err := os.ReadDir(markdownDocsDir)
// Delete old files
err = filepath.Walk(cliMarkdownDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
_, ok := wroteMap[path]
if !ok {
flog.Infof("deleting old doc\t %v", path)
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
if err != nil {
log.Fatal("Error on loading docs/cli files: ", err)
}
for _, file := range files {
// Remove file extension and prefix
title := strings.Replace(file.Name(), ".md", "", 1)
title = strings.Replace(title, "coder_", "", 1)
title = strings.Replace(title, "_", " ", -1)
cliRoutes = append(cliRoutes, route{
Title: title,
Path: "./cli/" + file.Name(),
})
flog.Fatalf("deleting old docs: %v", err)
}
// Read manifest
jsonFile, err := os.ReadFile(manifestFilepath)
err = deleteEmptyDirs(cliMarkdownDir)
if err != nil {
log.Fatal("Error on open docs/manifest.json: ", err)
}
var manifest manifest
err = json.Unmarshal(jsonFile, &manifest)
if err != nil {
log.Fatal("Error on unmarshal manifest.json: ", err)
flog.Fatalf("deleting empty dirs: %v", err)
}
// Update manifest
for i, r := range manifest.Routes {
if r.Title != "Command Line" {
manifestPath := filepath.Join(docsDir, "manifest.json")
manifestByt, err := os.ReadFile(manifestPath)
if err != nil {
flog.Fatalf("reading manifest: %v", err)
}
var manifest manifest
err = json.Unmarshal(manifestByt, &manifest)
if err != nil {
flog.Fatalf("unmarshalling manifest: %v", err)
}
var found bool
for i := range manifest.Routes {
rt := &manifest.Routes[i]
if rt.Title != "Command Line" {
continue
}
rt.Children = nil
found = true
for path, cmd := range wroteMap {
relPath, err := filepath.Rel(docsDir, path)
if err != nil {
flog.Fatalf("getting relative path: %v", err)
}
rt.Children = append(rt.Children, route{
Title: fullName(cmd),
Description: cmd.Short,
Path: relPath,
})
}
// Sort children by title because wroteMap iteration is
// non-deterministic.
sort.Slice(rt.Children, func(i, j int) bool {
return rt.Children[i].Title < rt.Children[j].Title
})
}
manifest.Routes[i].Children = cliRoutes
break
if !found {
flog.Fatalf("could not find Command Line route in manifest")
}
manifestFile, err := json.MarshalIndent(manifest, "", " ")
manifestByt, err = json.MarshalIndent(manifest, "", " ")
if err != nil {
log.Fatal("Error on marshal manifest.json: ", err)
flog.Fatalf("marshaling manifest: %v", err)
}
err = os.WriteFile(manifestFilepath, manifestFile, 0o644) // #nosec
err = os.WriteFile(manifestPath, manifestByt, 0o600)
if err != nil {
log.Fatal("Error on write update on manifest.json: ", err)
flog.Fatalf("writing manifest: %v", err)
}
}