-
Notifications
You must be signed in to change notification settings - Fork 17
/
gendoc.go
185 lines (168 loc) · 6.09 KB
/
gendoc.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Command gendoc generates godoc comments describing the usage of tools based
// on the cmdline package.
//
// Usage:
// go run gendoc.go [flags] <pkg> [args]
//
// <pkg> is the package path for the tool.
//
// [args] are the arguments to pass to the tool to produce usage output. If no
// args are given, runs "<tool> help ..."
//
// The reason this command is located under a testdata directory is to enforce
// its idiomatic use via "go run".
//
// The gendoc command itself is not based on the cmdline library to avoid
// non-trivial bootstrapping.
package main
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
var (
flagEnv string
flagInstall string
flagOut string
flagTags string
copyrightNotice string
goInstallCommand string
)
func main() {
flag.StringVar(&flagEnv, "env", "os", `Environment variables to set before running command. If "os", grabs vars from the underlying OS. If empty, doesn't set any vars. Otherwise vars are expected to be comma-separated entries of the form KEY1=VALUE1,KEY2=VALUE2,...`)
flag.StringVar(&flagInstall, "install", "", "Comma separated list of packages to install before running command. All commands that are built will be on the PATH.")
flag.StringVar(&flagOut, "out", "./doc.go", "Path to the output file.")
flag.StringVar(&flagTags, "tags", "", "Tags for go build, also added as build constraints in the generated output file.")
flag.StringVar(©rightNotice, "copyright-notice", "", "File containing the copyright notice to be prepended to the autogenerated documentation; if specified as an empty string then no copyright notice will be used.")
flag.StringVar(&goInstallCommand, "build-cmd", "", "Comand to use for building/installing commands whose usage is to be documented, it must accept the same flags as 'go install'.")
flag.Parse()
if err := generate(flag.Args()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func generate(args []string) error {
if got, want := len(args), 1; got < want {
return fmt.Errorf("gendoc requires at least one argument\nusage: gendoc <pkg> [args]")
}
pkg, args := args[0], args[1:]
// Find out the binary name from the pkg name.
var listOut bytes.Buffer
listCmd := exec.Command("go", "list", pkg)
listCmd.Stdout = &listOut
if err := listCmd.Run(); err != nil {
return fmt.Errorf("%q failed: %v\n%v\n", strings.Join(listCmd.Args, " "), err, listOut.String())
}
binName := filepath.Base(strings.TrimSpace(listOut.String()))
// Install all packages in a temporary directory.
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
return fmt.Errorf("TempDir() failed: %v", err)
}
defer os.RemoveAll(tmpDir)
pkgs := []string{pkg}
if flagInstall != "" {
pkgs = append(pkgs, strings.Split(flagInstall, ",")...)
}
installCmd := append([]string{}, "go", "install")
if len(goInstallCommand) > 0 {
installCmd = strings.Split(goInstallCommand, " ")
}
for _, installPkg := range pkgs {
installArgs := append(installCmd, "-tags="+flagTags, installPkg)
installCmd := exec.Command(installArgs[0], installArgs[1:]...)
installCmd.Env = append(os.Environ(), "GOBIN="+tmpDir)
if err := installCmd.Run(); err != nil {
return fmt.Errorf("%q failed: %v\n", strings.Join(installCmd.Args, " "), err)
}
}
// Run the binary to generate documentation.
var out bytes.Buffer
if len(args) == 0 {
args = []string{"help", "..."}
}
runCmd := exec.Command(filepath.Join(tmpDir, binName), args...)
runCmd.Stdout = &out
runCmd.Env = runEnviron(tmpDir)
if err := runCmd.Run(); err != nil {
return fmt.Errorf("%q failed: %v\n%v\n", strings.Join(runCmd.Args, " "), err, out.String())
}
var tagsConstraint string
if flagTags != "" {
tagsConstraint = fmt.Sprintf("// +build %s\n\n", flagTags)
}
copyright := `// Copyright 2018 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
`
flag.Visit(func(f *flag.Flag) {
if f.Name == "copyright-notice" {
copyright = ""
}
})
if len(copyright) == 0 {
if len(copyrightNotice) > 0 {
buf, err := ioutil.ReadFile(copyrightNotice)
if err != nil {
return fmt.Errorf("failed to read copyright notice file: %v: %v", copyrightNotice, err)
}
copyright = string(buf)
}
}
doc := fmt.Sprintf(`%s// This file was auto-generated via go generate.
// DO NOT UPDATE MANUALLY
%s/*
%s*/
package main
`, copyright, tagsConstraint, suppressParallelFlag(out.String()))
// Write the result to the output file.
path, perm := flagOut, os.FileMode(0644)
if err := ioutil.WriteFile(path, []byte(doc), perm); err != nil {
return fmt.Errorf("WriteFile(%v, %v) failed: %v\n", path, perm, err)
}
return nil
}
// suppressParallelFlag replaces the default value of the test.parallel flag
// with the literal string "<number of threads>". The default value of the
// test.parallel flag is GOMAXPROCS, which (since Go1.5) is set to the number
// of logical CPU threads on the current system. This causes problems with the
// vanadium-go-generate test, which requires that the output of gendoc is the
// same on all systems.
func suppressParallelFlag(input string) string {
pattern := regexp.MustCompile("(?m:(^ -test\\.parallel=)(?:\\d)+$)")
return pattern.ReplaceAllString(input, "$1<number of threads>")
}
// runEnviron returns the environment variables to use when running the command
// to retrieve full help information.
func runEnviron(binDir string) []string {
// Never return nil, which signals exec.Command to use os.Environ.
in, out := strings.Split(flagEnv, ","), make([]string, 0)
if flagEnv == "os" {
in = os.Environ()
}
updatedPath := false
for _, e := range in {
if e == "" {
continue
}
if strings.HasPrefix(e, "PATH=") {
e = "PATH=" + binDir + string(os.PathListSeparator) + e[5:]
updatedPath = true
}
out = append(out, e)
}
if !updatedPath {
out = append(out, "PATH="+binDir)
}
out = append(out, "CMDLINE_STYLE=godoc")
return out
}