├── example ├── example.png └── example.go ├── node.go ├── LICENSE ├── FORMAT.md ├── README.md ├── mango.go ├── file.go ├── writer.go ├── reader.go └── flag.go /example/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slyrz/mango/HEAD/example/example.png -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type NodeType uint32 4 | 5 | const ( 6 | DocumentNode NodeType = iota 7 | BlockNode 8 | BreakNode 9 | ListNode 10 | SectionNode 11 | TextBoldNode 12 | TextNode 13 | TextUnderlineNode 14 | ) 15 | 16 | type Node struct { 17 | Type NodeType 18 | Text string 19 | Childs []*Node 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, the mango developers. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | // example - shows the basic usage of mango 2 | // 3 | // Description: 4 | // 5 | // It doesn't take much to create manual pages with mango. Just write down 6 | // stuff you want to include in the manual page in a comment at the top 7 | // of your source file like this. Feel free to add as many sections as you 8 | // want. 9 | package main 10 | 11 | import ( 12 | "flag" 13 | ) 14 | 15 | var ( 16 | optFoo = flag.Bool("foo", false, "This text should show up in the manual page.") 17 | 18 | // If the flag declaration follows a comment like this, mango displays the 19 | // comment as description in the manual page. 20 | optBar = flag.Bool("bar", false, "The above comment should show up in the manual page.") 21 | optBaz = "" 22 | ) 23 | 24 | func init() { 25 | // These two calls reference the same variable and will appear 26 | // grouped in the manual page. Since these aren't boolean flags, mango 27 | // prints the argument type as well. 28 | flag.StringVar(&optBaz, "baz", "", "Two calls, but one entry in the manual.") 29 | flag.StringVar(&optBaz, "b", "", "Two calls, but one entry in the manual.") 30 | } 31 | 32 | func main() { 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /FORMAT.md: -------------------------------------------------------------------------------- 1 | ## Formatting 2 | 3 | mango supports comment formatting in a Markdown-like syntax. 4 | 5 | ### Headings 6 | 7 | ``` 8 | First Heading: 9 | 10 | Riverrun, past Eve and Adam's, from swerve of shore to bend 11 | of bay, brings us by a commodius vicus of recirculation 12 | back to Howth Castle and Environs. 13 | 14 | Second Heading 15 | ============== 16 | Riverrun, past Eve and Adam's, from swerve of shore to bend 17 | of bay, brings us by a commodius vicus of recirculation 18 | back to Howth Castle and Environs. 19 | 20 | Third Heading 21 | ------------- 22 | Riverrun, past Eve and Adam's, from swerve of shore to bend 23 | of bay, brings us by a commodius vicus of recirculation 24 | back to Howth Castle and Environs. 25 | ``` 26 | 27 | ### Paragraphs 28 | 29 | Paragraphs are separated by a blank line. 30 | 31 | ``` 32 | Riverrun, past Eve and Adam's, from swerve of shore to bend 33 | of bay, brings us by a commodius vicus of recirculation 34 | back to Howth Castle and Environs. 35 | 36 | Sir Tristram, violer d'amores, fr'over the short sea, 37 | had passencore rearrived from North Armorica on this side the scraggy 38 | isthmus of Europe Minor to wielderfight his penisolate war. 39 | ``` 40 | 41 | ### Emphasis 42 | 43 | Asterisks (\*) and underscores (\_) are indicators of emphasis. 44 | 45 | ``` 46 | *Wassaily Booslaeugh* of _Riesengeborg_ 47 | ``` 48 | 49 | ### Code 50 | 51 | Code blocks begin with a closing angle bracket (>). 52 | 53 | ``` 54 | > echo "Kick nuck, Knockcastle" 55 | ``` 56 | 57 | ### Lists 58 | 59 | Asterisks, numbers or single words followed by a closing parenthesis 60 | at the start of a line create list items. 61 | 62 | ``` 63 | *) Item 64 | *) Item 65 | *) Item 66 | 67 | 1) Item 68 | 2) Item 69 | 3) Item 70 | 71 | a) Item 72 | b) Item 73 | c) Item 74 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mango 2 | 3 | *Generate manual pages from the source code of your Go commands* 4 | 5 | mango is a small command line utility that allows you to create manual 6 | pages from the source code of your Go commands. It builds manual pages from 7 | the comments and *flag* function calls found in your .go files. 8 | 9 | #### Building 10 | 11 | Execute 12 | ```bash 13 | go get github.com/slyrz/mango 14 | go build github.com/slyrz/mango 15 | ``` 16 | to build mango. 17 | 18 | #### Running 19 | 20 | Pass one or more .go files as command line arguments to mango. 21 | mango treats them as a list of independent Go commands and creates a 22 | manual page for each argument. 23 | 24 | ```bash 25 | mango file1.go file2.go ... 26 | ``` 27 | 28 | ## Usage 29 | 30 | #### Source File 31 | 32 | ```go 33 | // example - shows the basic usage of mango 34 | // 35 | // Description: 36 | // 37 | // It doesn't take much to create manual pages with mango. Just write down 38 | // stuff you want to include in the manual page in a comment at the top 39 | // of your source file like this. Feel free to add as many sections as you 40 | // want. 41 | package main 42 | 43 | import ( 44 | "flag" 45 | ) 46 | 47 | var ( 48 | optFoo = flag.Bool("foo", false, "This text should show up in the manual page.") 49 | 50 | // If the flag declaration follows a comment like this, mango displays the 51 | // comment as description in the manual page. 52 | optBar = flag.Bool("bar", false, "The above comment should show up in the manual page.") 53 | optBaz = "" 54 | ) 55 | 56 | func init() { 57 | // These two calls reference the same variable and will appear 58 | // grouped in the manual page. Since these aren't boolean flags, mango 59 | // prints the argument type as well. 60 | flag.StringVar(&optBaz, "baz", "", "Two calls, but one entry in the manual.") 61 | flag.StringVar(&optBaz, "b", "", "Two calls, but one entry in the manual.") 62 | } 63 | 64 | func main() { 65 | return 66 | } 67 | ``` 68 | 69 | #### Result 70 | 71 | ![](example/example.png) 72 | 73 | ### License 74 | 75 | mango is released under MIT license. 76 | You can find a copy of the MIT License in the [LICENSE](./LICENSE) file. 77 | -------------------------------------------------------------------------------- /mango.go: -------------------------------------------------------------------------------- 1 | // mango - generate manual pages from Go source code 2 | // 3 | // Description: 4 | // 5 | // mango generates manual pages from the source code of Go commands. 6 | // It aims to generate full-fledged manual pages soley based on the comments 7 | // and flag function calls found inside Go source code. 8 | // 9 | // See Also: 10 | // 11 | // man(1), man-pages(7) 12 | package main 13 | 14 | import ( 15 | "flag" 16 | "os" 17 | "os/exec" 18 | ) 19 | 20 | var options = struct { 21 | Output string 22 | Name string 23 | Plain bool 24 | Preview bool 25 | }{} 26 | 27 | func init() { 28 | // Write the manual page to file. 29 | flag.StringVar(&options.Output, "output", "", "write to `file`") 30 | // Set the manual page title to name. 31 | flag.StringVar(&options.Name, "title", "", "set title to `name`") 32 | // Ignore markup inside comments. 33 | flag.BoolVar(&options.Plain, "plain", false, "treat comments as plain text") 34 | // Preview the manual page with the man command. 35 | flag.BoolVar(&options.Preview, "preview", false, "preview with man") 36 | } 37 | 38 | func getReader() Reader { 39 | if options.Plain { 40 | return NewPlainReader() 41 | } else { 42 | return NewMarkupReader() 43 | } 44 | } 45 | 46 | func getWriter() Writer { 47 | return NewTroffWriter() 48 | } 49 | 50 | func main() { 51 | flag.Parse() 52 | 53 | builder := NewBuilder(getReader(), getWriter()) 54 | for _, arg := range flag.Args() { 55 | file, err := NewFile(arg) 56 | if err != nil { 57 | panic(err) 58 | } 59 | if options.Name != "" { 60 | file.Name = options.Name 61 | } 62 | text, err := builder.Build(file) 63 | if err != nil { 64 | panic(err) 65 | } 66 | if options.Preview { 67 | cmd := exec.Command("groff", "-Wall", "-mtty-char", "-mandoc", "-Tascii") 68 | cmd.Stdout = os.Stdout 69 | cmd.Stderr = os.Stderr 70 | inp, err := cmd.StdinPipe() 71 | if err != nil { 72 | panic(err) 73 | } 74 | if err := cmd.Start(); err != nil { 75 | panic(err) 76 | } 77 | inp.Write([]byte(text)) 78 | inp.Close() 79 | cmd.Wait() 80 | } else { 81 | if options.Output == "" { 82 | os.Stdout.Write([]byte(text)) 83 | } else { 84 | dst, err := os.Create(options.Output) 85 | if err != nil { 86 | panic(err) 87 | } 88 | dst.Write([]byte(text)) 89 | dst.Close() 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "os" 9 | "path" 10 | "time" 11 | ) 12 | 13 | var ErrFileType = errors.New("not a Go file") 14 | 15 | // File represents a parsed '.go' source file. 16 | type File struct { 17 | Path string // Path to file. 18 | Name string // Name of command. 19 | Time time.Time // Modification time. 20 | Flags []*Flag // Flags found in file. 21 | Doc string // Comment preceding the "package" keyword. 22 | } 23 | 24 | func splitExt(s string) (string, string) { 25 | i := len(s) - len(path.Ext(s)) 26 | return s[:i], s[i:] 27 | } 28 | 29 | func NewFile(path string) (*File, error) { 30 | info, err := os.Stat(path) 31 | if err != nil { 32 | return nil, err 33 | } 34 | name, ext := splitExt(info.Name()) 35 | if ext != ".go" { 36 | return nil, ErrFileType 37 | } 38 | file := &File{ 39 | Path: path, 40 | Name: name, 41 | Time: info.ModTime(), 42 | } 43 | return file, file.parseFlags() 44 | } 45 | 46 | // parseFlags transforms all flag package calls to Flags. 47 | func (f *File) parseFlags() error { 48 | set := token.NewFileSet() 49 | file, err := parser.ParseFile(set, f.Path, nil, parser.ParseComments) 50 | if err != nil { 51 | return err 52 | } 53 | // The last comment group before a package declaration must contain the 54 | // command description. 55 | packageLine := 2 56 | if packagePos := set.Position(file.Package); packagePos.IsValid() { 57 | packageLine = packagePos.Line 58 | } 59 | // Load comment groups and map them to their ending line number. 60 | // Assume a comment belongs to a command line flag declaration if it 61 | // ends on the previous line of the flag declaration. 62 | comments := make(map[int]*ast.CommentGroup) 63 | for _, group := range file.Comments { 64 | pos := set.Position(group.Pos()) 65 | end := set.Position(group.End()) 66 | if pos.Line < packageLine { 67 | f.Doc = group.Text() 68 | } 69 | comments[end.Line] = group 70 | } 71 | // Memorize flags by their variable names. 72 | bound := make(map[string]*Flag) 73 | // Collect all flags in source file. 74 | ast.Inspect(file, func(node ast.Node) bool { 75 | if call, ok := node.(*ast.CallExpr); ok { 76 | if opt, err := NewFlag(set, call); err == nil { 77 | // Check if we have a comment that belongs to flag 78 | if comment, ok := comments[opt.Line-1]; ok { 79 | opt.Doc = comment.Text() 80 | } 81 | // Check if we already encountered an flag bound to the 82 | // variable. 83 | if opt.Variable != "" { 84 | if reg, ok := bound[opt.Variable]; ok { 85 | // Merge currrent flag with the one we already found 86 | reg.merge(opt) 87 | // Don't add the current flag to the list, since the list 88 | // already contains the struct stored in the map. 89 | return true 90 | } else { 91 | // Register variable and the proceed to add flag 92 | // struct to the flags list 93 | bound[opt.Variable] = opt 94 | } 95 | } 96 | f.Flags = append(f.Flags, opt) 97 | } 98 | } 99 | return true 100 | }) 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Writer interface { 11 | Meta(string, time.Time) 12 | Block(string) 13 | Break(string) 14 | List(string) 15 | Section(string) 16 | Text(string) 17 | TextBold(string) 18 | TextUnderline(string) 19 | Flag(string, string, string) 20 | 21 | Done() string 22 | } 23 | 24 | var manSections = []string{ 25 | "name", 26 | "synopsis", 27 | "description", 28 | "options", 29 | "exit status", 30 | } 31 | 32 | type Troff struct { 33 | sections map[string]string 34 | order []string 35 | active string 36 | buffer bytes.Buffer 37 | name string 38 | date string 39 | } 40 | 41 | func NewTroffWriter() *Troff { 42 | return &Troff{ 43 | sections: make(map[string]string), 44 | } 45 | } 46 | 47 | func (tr *Troff) Done() string { 48 | if tr.buffer.Len() > 0 { 49 | tr.sections[tr.active] = tr.buffer.String() 50 | } 51 | tr.buffer.Reset() 52 | 53 | // Generates a Linux style man page title line: 54 | // 1. Title of man page in all caps 55 | // 2. Section number 56 | // 3. Date in YYYY-MM-DD format (footer, middle) 57 | // 4. Source of the command (footer, left) 58 | // 5. Title of the manual (header, center) 59 | tr.writeln(`.TH "%[1]s" 1 "%[3]s" "%[2]s" "%[2]s Manual"`, strings.ToUpper(tr.name), tr.name, tr.date) 60 | 61 | // At first, render special Manpage sections in their usual order. 62 | for _, section := range manSections { 63 | if output, ok := tr.sections[section]; ok { 64 | tr.write(output) 65 | delete(tr.sections, section) 66 | } 67 | } 68 | 69 | // Now render the remaining sections in the order they appeard in the 70 | // source file. 71 | for _, section := range tr.order { 72 | if output, ok := tr.sections[section]; ok { 73 | tr.write(output) 74 | } 75 | } 76 | 77 | return tr.buffer.String() 78 | } 79 | 80 | func (tr *Troff) write(format string, args ...interface{}) { 81 | fmt.Fprintf(&tr.buffer, format, args...) 82 | } 83 | 84 | func (tr *Troff) writeln(format string, args ...interface{}) { 85 | fmt.Fprintf(&tr.buffer, format+"\n", args...) 86 | } 87 | 88 | func (tr *Troff) Meta(name string, date time.Time) { 89 | tr.name = strings.Title(name) 90 | tr.date = date.Format("2006-01-02") 91 | } 92 | 93 | func (tr *Troff) Break(text string) { 94 | tr.writeln(".PP") 95 | } 96 | 97 | func (tr *Troff) Block(text string) { 98 | tr.writeln(".RS 4") 99 | tr.writeln(".nf") 100 | tr.writeln(text) 101 | tr.writeln(".fi") 102 | tr.writeln(".RE") 103 | } 104 | 105 | func (tr *Troff) List(text string) { 106 | tr.writeln(".TP") 107 | if text == "*" { 108 | tr.writeln(`\(bu`) 109 | } else { 110 | tr.writeln(`.B "%s"`, text) 111 | } 112 | } 113 | 114 | func (tr *Troff) Section(text string) { 115 | if tr.buffer.Len() > 0 { 116 | tr.sections[tr.active] = tr.buffer.String() 117 | } 118 | tr.buffer.Reset() 119 | tr.active = strings.ToLower(text) 120 | tr.order = append(tr.order, tr.active) 121 | 122 | tr.writeln(`.SH "%s"`, strings.ToUpper(text)) 123 | } 124 | 125 | func (tr *Troff) Text(text string) { 126 | text = strings.TrimSpace(text) 127 | if text != "" { 128 | tr.writeln(text) 129 | } 130 | } 131 | 132 | func (tr *Troff) TextBold(text string) { 133 | tr.writeln(`.B "%s"`, strings.TrimSpace(text)) 134 | } 135 | 136 | func (tr *Troff) TextUnderline(text string) { 137 | tr.writeln(`.I "%s"`, strings.TrimSpace(text)) 138 | } 139 | 140 | func (tr *Troff) Flag(name, short, param string) { 141 | tr.writeln(".TP") 142 | if short != "" { 143 | tr.write(`.B \-%s -%s`, short, name) 144 | } else { 145 | tr.write(`.B \-%s`, name) 146 | } 147 | if param != "" { 148 | tr.write(` \fI%s\fR`, param) 149 | } 150 | tr.writeln("") 151 | 152 | } 153 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type Reader interface { 10 | Read(string) (*Node, error) 11 | } 12 | 13 | func skip(text string, n int) string { 14 | if n < 0 || n >= len(text) { 15 | return "" 16 | } 17 | return text[n:] 18 | } 19 | 20 | func nextLine(text string) (string, string) { 21 | end := strings.IndexRune(text, '\n') 22 | if end == -1 { 23 | return text, "" 24 | } 25 | return text[:end], text[end+1:] 26 | } 27 | 28 | type MarkupReader struct { 29 | // empty 30 | } 31 | 32 | func NewMarkupReader() *MarkupReader { 33 | return &MarkupReader{} 34 | } 35 | 36 | var markupEmph = map[rune]NodeType{ 37 | '*': TextBoldNode, 38 | '_': TextUnderlineNode, 39 | '`': TextNode, 40 | } 41 | 42 | // parseText transforms text into TextNode, TextBoldNode and TextUnderlineNode. 43 | func (m *MarkupReader) parseText(text string, callback func(NodeType, string)) { 44 | var ( 45 | buff bytes.Buffer 46 | skip = -1 47 | ) 48 | for i, r := range text { 49 | if i <= skip { 50 | continue 51 | } 52 | if k, ok := markupEmph[r]; ok { 53 | if end := i + 1 + strings.IndexRune(text[i+1:], r); end > i { 54 | if buff.Len() > 0 { 55 | callback(TextNode, buff.String()) 56 | buff.Reset() 57 | } 58 | callback(k, text[i+1:end]) 59 | skip = end 60 | continue 61 | } 62 | } 63 | buff.WriteRune(r) 64 | } 65 | if buff.Len() > 0 { 66 | callback(TextNode, buff.String()) 67 | } 68 | } 69 | 70 | var markupMatchers = []struct { 71 | Regex *regexp.Regexp 72 | Multiline bool 73 | Type NodeType 74 | }{ 75 | {regexp.MustCompile(`^((\w+\s*)+)\n(?:-+|=+)\n`), true, SectionNode}, 76 | {regexp.MustCompile(`^(([A-Z]\w+\s*)+):\n\n`), true, SectionNode}, 77 | {regexp.MustCompile(`^(\w+|\*)\)\s*`), false, ListNode}, 78 | {regexp.MustCompile(`^(?:\>|\ {4}|\t)\s?(.*)\n`), true, BlockNode}, 79 | {regexp.MustCompile(`^(\n+)`), true, BreakNode}, 80 | } 81 | 82 | func (m *MarkupReader) parse(text string, callback func(NodeType, string)) { 83 | var line string 84 | OuterLoop: 85 | for len(text) > 0 { 86 | InnerLoop: 87 | for _, matcher := range markupMatchers { 88 | match := matcher.Regex.FindStringSubmatch(text) 89 | if match == nil { 90 | continue 91 | } 92 | all := match[0] 93 | val := match[1] 94 | callback(matcher.Type, val) 95 | // Skip the matching part. 96 | text = skip(text, len(all)) 97 | // If the matcher consumes full lines, text is at the beginning 98 | // of a new line and we go back to the OuterLoop. 99 | if matcher.Multiline { 100 | continue OuterLoop 101 | } else { 102 | break InnerLoop 103 | } 104 | } 105 | line, text = nextLine(text) 106 | m.parseText(line, callback) 107 | } 108 | } 109 | 110 | func (m *MarkupReader) Read(text string) (*Node, error) { 111 | root := &Node{Type: DocumentNode} 112 | curr := root 113 | m.parse(text, func(kind NodeType, text string) { 114 | node := &Node{Type: kind, Text: text} 115 | if kind == SectionNode { 116 | root.Childs = append(root.Childs, node) 117 | curr = node 118 | } else { 119 | curr.Childs = append(curr.Childs, node) 120 | } 121 | }) 122 | return root, nil 123 | } 124 | 125 | type PlainReader struct { 126 | // empty 127 | } 128 | 129 | func NewPlainReader() *PlainReader { 130 | return &PlainReader{} 131 | } 132 | 133 | func (p *PlainReader) Read(text string) (*Node, error) { 134 | root := &Node{Type: DocumentNode} 135 | empty := 0 136 | for _, line := range strings.Split(text, "\n") { 137 | line = strings.TrimSpace(line) 138 | if line != "" { 139 | if empty > 0 { 140 | root.Childs = append(root.Childs, &Node{Type: BreakNode}) 141 | } 142 | root.Childs = append(root.Childs, &Node{Type: TextNode, Text: line}) 143 | empty = 0 144 | } else { 145 | empty++ 146 | } 147 | } 148 | return root, nil 149 | } 150 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "go/ast" 6 | "go/token" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | ErrNotSupported = errors.New("not supported") 13 | ErrParse = errors.New("parser error") 14 | ) 15 | 16 | var ( 17 | regexFlagCall = regexp.MustCompile(`^[Ff]lags?\.(Bool|Duration|Float|Float64|Int|Int64|String|Uint|Uint64)(Var)?$`) 18 | regexBackquote = regexp.MustCompile("`(.*)`") 19 | ) 20 | 21 | type FunctionCall struct { 22 | Line int 23 | Expr string 24 | Name string 25 | Args []string 26 | } 27 | 28 | func NewFunctionCall(fs *token.FileSet, n *ast.CallExpr) (*FunctionCall, error) { 29 | parts := make([]string, 0) 30 | ast.Inspect(n.Fun, func(n ast.Node) bool { 31 | if n == nil { 32 | return true 33 | } 34 | switch obj := n.(type) { 35 | case *ast.SelectorExpr: 36 | // do nothing, just avoid the default case 37 | break 38 | case *ast.Ident: 39 | parts = append(parts, obj.Name) 40 | default: 41 | return false 42 | } 43 | return true 44 | }) 45 | if len(parts) == 0 { 46 | return nil, ErrNotSupported 47 | } 48 | args := make([]string, len(n.Args)) 49 | for i, arg := range n.Args { 50 | ast.Inspect(arg, func(n ast.Node) bool { 51 | switch t := n.(type) { 52 | case *ast.Ident: 53 | args[i] = t.Name 54 | case *ast.BasicLit: 55 | args[i] = t.Value 56 | } 57 | return true 58 | }) 59 | } 60 | return &FunctionCall{ 61 | Line: fs.Position(n.Pos()).Line, 62 | Expr: strings.Join(parts, "."), 63 | Name: parts[len(parts)-1], 64 | Args: args, 65 | }, nil 66 | } 67 | 68 | type FlagType uint32 69 | 70 | const ( 71 | UnkownFlag FlagType = iota 72 | BoolFlag 73 | DurationFlag 74 | FloatFlag 75 | IntFlag 76 | UintFlag 77 | StringFlag 78 | ) 79 | 80 | var flagTypes = map[string]FlagType{ 81 | "Bool": BoolFlag, 82 | "Duration": DurationFlag, 83 | "Float": FloatFlag, 84 | "Float64": FloatFlag, 85 | "Int": IntFlag, 86 | "Int64": IntFlag, 87 | "String": StringFlag, 88 | "Uint": UintFlag, 89 | "Uint64": UintFlag, 90 | } 91 | 92 | // flagParam maps flag types to their default parameter names. These 93 | // parameter names will be used if the usage string does not contain a 94 | // backquoted name. 95 | var flagParam = map[FlagType]string{ 96 | DurationFlag: "duration", 97 | FloatFlag: "float", 98 | IntFlag: "int", 99 | StringFlag: "string", 100 | UintFlag: "uint", 101 | } 102 | 103 | type Flag struct { 104 | Type FlagType 105 | Line int 106 | Variable string // Pointer name (only set for ...Var() calls) 107 | Name string // Name of the flag 108 | Short string // Shorthand name of the flag 109 | Usage string // Usage of the flag 110 | Param string // User specified parameter name (back-quoted word in usage) 111 | Doc string // Comment above flag declaration 112 | } 113 | 114 | func trimQuotes(s string) string { 115 | if len(s) == 0 { 116 | return "" 117 | } 118 | if (s[0] == '`' || s[0] == '"') && s[0] == s[len(s)-1] { 119 | s = s[1 : len(s)-1] 120 | } 121 | return s 122 | } 123 | 124 | func NewFlag(fs *token.FileSet, n *ast.CallExpr) (*Flag, error) { 125 | call, err := NewFunctionCall(fs, n) 126 | if err != nil { 127 | return nil, err 128 | } 129 | match := regexFlagCall.FindStringSubmatch(call.Expr) 130 | if match == nil { 131 | return nil, ErrNotSupported 132 | } 133 | // Pad to 4 arguments. 134 | if len(call.Args) == 3 { 135 | call.Args = append([]string{""}, call.Args...) 136 | } 137 | result := &Flag{ 138 | Type: flagTypes[match[1]], 139 | Line: call.Line, 140 | Variable: call.Args[0], 141 | Name: trimQuotes(call.Args[1]), 142 | Usage: trimQuotes(call.Args[3]), 143 | } 144 | // Check if there's a backquoted parameter name in the usage string. 145 | if match := regexBackquote.FindStringSubmatch(result.Usage); match != nil { 146 | result.Param = match[1] 147 | result.Usage = strings.Replace(result.Usage, "`", "", -1) 148 | } else { 149 | result.Param = flagParam[result.Type] 150 | } 151 | return result, nil 152 | } 153 | 154 | func assignIfEmpty(d *string, v string) { 155 | if *d == "" { 156 | *d = v 157 | } 158 | } 159 | 160 | func (o *Flag) merge(v *Flag) { 161 | if len(o.Name) < len(v.Name) { 162 | o.Short = o.Name 163 | o.Name = v.Name 164 | } else { 165 | o.Short = v.Name 166 | } 167 | assignIfEmpty(&o.Doc, v.Doc) 168 | assignIfEmpty(&o.Param, v.Param) 169 | assignIfEmpty(&o.Usage, v.Usage) 170 | } 171 | --------------------------------------------------------------------------------