commit
34293148d7
|
@ -0,0 +1 @@
|
||||||
|
.idea/
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2021 John Olheiser
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,7 @@
|
||||||
|
# ffmd
|
||||||
|
|
||||||
|
[![Go Reference](https://pkg.go.dev/badge/go.jolheiser.com/ffmd.svg)](https://pkg.go.dev/go.jolheiser.com/ffmd)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
|
@ -0,0 +1,367 @@
|
||||||
|
# myapp
|
||||||
|
|
||||||
|
myapp
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp
|
||||||
|
├─ sub1
|
||||||
|
├─ sub2
|
||||||
|
│ ├─ sub3
|
||||||
|
│ │ └─ sub4
|
||||||
|
│ └─ sub5
|
||||||
|
└─ sub6
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--myapp-bool-flag-f]
|
||||||
|
[--myapp-bool-flag-t]
|
||||||
|
[--myapp-duration-flag]=[value]
|
||||||
|
[--myapp-duration-flag-default]=[value]
|
||||||
|
[--myapp-int-flag]=[value]
|
||||||
|
[--myapp-int-flag-default]=[value]
|
||||||
|
[--myapp-string-flag]=[value]
|
||||||
|
[--myapp-string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-string-flag-default**="": String flag with default (default: `string default`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## sub1
|
||||||
|
|
||||||
|
sub1
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--sub1-bool-flag-f]
|
||||||
|
[--sub1-bool-flag-t]
|
||||||
|
[--sub1-duration-flag]=[value]
|
||||||
|
[--sub1-duration-flag-default]=[value]
|
||||||
|
[--sub1-int-flag]=[value]
|
||||||
|
[--sub1-int-flag-default]=[value]
|
||||||
|
[--sub1-string-flag]=[value]
|
||||||
|
[--sub1-string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
root [FLAGS] sub1 [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--sub1-bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--sub1-bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub1-duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub1-duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub1-int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub1-int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub1-string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub1-string-flag-default**="": String flag with default (default: `string default`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## sub2
|
||||||
|
|
||||||
|
Short help
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--sub2-bool-flag-f]
|
||||||
|
[--sub2-bool-flag-t]
|
||||||
|
[--sub2-duration-flag]=[value]
|
||||||
|
[--sub2-duration-flag-default]=[value]
|
||||||
|
[--sub2-int-flag]=[value]
|
||||||
|
[--sub2-int-flag-default]=[value]
|
||||||
|
[--sub2-string-flag]=[value]
|
||||||
|
[--sub2-string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
root [FLAGS] sub2 [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--sub2-bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--sub2-bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub2-duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub2-duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub2-int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub2-int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub2-string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub2-string-flag-default**="": String flag with default (default: `string default`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
### sub3
|
||||||
|
|
||||||
|
Long help
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--sub3-bool-flag-f]
|
||||||
|
[--sub3-bool-flag-t]
|
||||||
|
[--sub3-duration-flag]=[value]
|
||||||
|
[--sub3-duration-flag-default]=[value]
|
||||||
|
[--sub3-int-flag]=[value]
|
||||||
|
[--sub3-int-flag-default]=[value]
|
||||||
|
[--sub3-string-flag]=[value]
|
||||||
|
[--sub3-string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
root [FLAGS] sub2 [FLAGS] sub3 [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--sub3-bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--sub3-bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub3-duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub3-duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub3-int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub3-int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub3-string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub3-string-flag-default**="": String flag with default (default: `string default`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
#### sub4
|
||||||
|
|
||||||
|
sub4
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--sub4-bool-flag-f]
|
||||||
|
[--sub4-bool-flag-t]
|
||||||
|
[--sub4-duration-flag]=[value]
|
||||||
|
[--sub4-duration-flag-default]=[value]
|
||||||
|
[--sub4-int-flag]=[value]
|
||||||
|
[--sub4-int-flag-default]=[value]
|
||||||
|
[--sub4-string-flag]=[value]
|
||||||
|
[--sub4-string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
sub4 [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--sub4-bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--sub4-bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub4-duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub4-duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub4-int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub4-int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub4-string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub4-string-flag-default**="": String flag with default (default: `string default`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
### sub5
|
||||||
|
|
||||||
|
sub5
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--sub5-bool-flag-f]
|
||||||
|
[--sub5-bool-flag-t]
|
||||||
|
[--sub5-duration-flag]=[value]
|
||||||
|
[--sub5-duration-flag-default]=[value]
|
||||||
|
[--sub5-int-flag]=[value]
|
||||||
|
[--sub5-int-flag-default]=[value]
|
||||||
|
[--sub5-string-flag]=[value]
|
||||||
|
[--sub5-string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
sub5 [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--sub5-bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--sub5-bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub5-duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub5-duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub5-int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub5-int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub5-string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub5-string-flag-default**="": String flag with default (default: `string default`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## sub6
|
||||||
|
|
||||||
|
sub6
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--sub6-bool-flag-f]
|
||||||
|
[--sub6-bool-flag-t]
|
||||||
|
[--sub6-duration-flag]=[value]
|
||||||
|
[--sub6-duration-flag-default]=[value]
|
||||||
|
[--sub6-int-flag]=[value]
|
||||||
|
[--sub6-int-flag-default]=[value]
|
||||||
|
[--sub6-string-flag]=[value]
|
||||||
|
[--sub6-string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
sub6 [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--sub6-bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--sub6-bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub6-duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub6-duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub6-int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub6-int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--sub6-string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--sub6-string-flag-default**="": String flag with default (default: `string default`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
# myapp
|
||||||
|
|
||||||
|
myapp
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--myapp-bool-flag-f]
|
||||||
|
[--myapp-bool-flag-t]
|
||||||
|
[--myapp-duration-flag]=[value]
|
||||||
|
[--myapp-duration-flag-default]=[value]
|
||||||
|
[--myapp-int-flag]=[value]
|
||||||
|
[--myapp-int-flag-default]=[value]
|
||||||
|
[--myapp-string-flag]=[value]
|
||||||
|
[--myapp-string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--myapp-string-flag-default**="": String flag with default (default: `string default`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
//go:build generate
|
||||||
|
// +build generate
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
|
||||||
|
"go.jolheiser.com/ffmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go run examples.go
|
||||||
|
func main() {
|
||||||
|
fs := flagSet("")
|
||||||
|
md, err := ffmd.FromFlagSet(fs)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
write("flagset.md", md)
|
||||||
|
|
||||||
|
command()
|
||||||
|
commandSub()
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(path, content string) {
|
||||||
|
fi, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer fi.Close()
|
||||||
|
if _, err := fi.WriteString(content); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagSet(name string) *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("myapp", flag.ExitOnError)
|
||||||
|
fs.String(fmt.Sprintf("%sstring-flag", name), "", "String flag with no default")
|
||||||
|
fs.String(fmt.Sprintf("%sstring-flag-default", name), "string default", "String flag with default")
|
||||||
|
fs.Int(fmt.Sprintf("%sint-flag", name), 0, "Int flag with no default")
|
||||||
|
fs.Int(fmt.Sprintf("%sint-flag-default", name), 100, "Int flag with default")
|
||||||
|
fs.Bool(fmt.Sprintf("%sbool-flag-f", name), false, "Bool flag false")
|
||||||
|
fs.Bool(fmt.Sprintf("%sbool-flag-t", name), true, "Bool flag true")
|
||||||
|
fs.Duration(fmt.Sprintf("%sduration-flag", name), 0, "Duration flag with no default")
|
||||||
|
fs.Duration(fmt.Sprintf("%sduration-flag-default", name), time.Minute*5, "Duration flag with default")
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func command() {
|
||||||
|
fs1 := flagSet("myapp-")
|
||||||
|
cmd1 := &ffcli.Command{
|
||||||
|
Name: "myapp",
|
||||||
|
FlagSet: fs1,
|
||||||
|
Subcommands: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
md, err := ffmd.FromCommand(cmd1)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
write("command.md", md)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandSub() {
|
||||||
|
fs := flagSet("myapp-")
|
||||||
|
cmd := &ffcli.Command{
|
||||||
|
Name: "myapp",
|
||||||
|
FlagSet: fs,
|
||||||
|
}
|
||||||
|
fs1 := flagSet("sub1-")
|
||||||
|
cmd1 := &ffcli.Command{
|
||||||
|
Name: "sub1",
|
||||||
|
ShortUsage: "root [FLAGS] sub1 [FLAGS] [ARGS...]",
|
||||||
|
FlagSet: fs1,
|
||||||
|
}
|
||||||
|
fs2 := flagSet("sub2-")
|
||||||
|
cmd2 := &ffcli.Command{
|
||||||
|
Name: "sub2",
|
||||||
|
ShortUsage: "root [FLAGS] sub2 [FLAGS] [ARGS...]",
|
||||||
|
ShortHelp: "Short help",
|
||||||
|
FlagSet: fs2,
|
||||||
|
}
|
||||||
|
fs3 := flagSet("sub3-")
|
||||||
|
cmd3 := &ffcli.Command{
|
||||||
|
Name: "sub3",
|
||||||
|
ShortUsage: "root [FLAGS] sub2 [FLAGS] sub3 [FLAGS] [ARGS...]",
|
||||||
|
ShortHelp: "Short help",
|
||||||
|
LongHelp: "Long help",
|
||||||
|
FlagSet: fs3,
|
||||||
|
}
|
||||||
|
fs4 := flagSet("sub4-")
|
||||||
|
cmd4 := &ffcli.Command{
|
||||||
|
Name: "sub4",
|
||||||
|
FlagSet: fs4,
|
||||||
|
}
|
||||||
|
fs5 := flagSet("sub5-")
|
||||||
|
cmd5 := &ffcli.Command{
|
||||||
|
Name: "sub5",
|
||||||
|
FlagSet: fs5,
|
||||||
|
}
|
||||||
|
fs6 := flagSet("sub6-")
|
||||||
|
cmd6 := &ffcli.Command{
|
||||||
|
Name: "sub6",
|
||||||
|
FlagSet: fs6,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Subcommands = []*ffcli.Command{cmd1, cmd2, cmd6}
|
||||||
|
cmd2.Subcommands = []*ffcli.Command{cmd3, cmd5}
|
||||||
|
cmd3.Subcommands = []*ffcli.Command{cmd4}
|
||||||
|
|
||||||
|
md, err := ffmd.FromCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
write("command-sub.md", md)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
## myapp
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--bool-flag-f]
|
||||||
|
[--bool-flag-t]
|
||||||
|
[--duration-flag]=[value]
|
||||||
|
[--duration-flag-default]=[value]
|
||||||
|
[--help]
|
||||||
|
[--int-flag]=[value]
|
||||||
|
[--int-flag-default]=[value]
|
||||||
|
[--string-flag]=[value]
|
||||||
|
[--string-flag-default]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp [FLAGS] [ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--bool-flag-f**: Bool flag false
|
||||||
|
|
||||||
|
|
||||||
|
**--bool-flag-t**: Bool flag true (default: `true`)
|
||||||
|
|
||||||
|
|
||||||
|
**--duration-flag**="": Duration flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--duration-flag-default**="": Duration flag with default (default: `5m0s`)
|
||||||
|
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--int-flag**="": Int flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--int-flag-default**="": Int flag with default (default: `100`)
|
||||||
|
|
||||||
|
|
||||||
|
**--string-flag**="": String flag with no default
|
||||||
|
|
||||||
|
|
||||||
|
**--string-flag-default**="": String flag with default (default: `string default`)
|
|
@ -0,0 +1,146 @@
|
||||||
|
package ffmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed ffmd.tmpl
|
||||||
|
ffmdtmpl string
|
||||||
|
Template = template.Must(template.New("").Parse(ffmdtmpl))
|
||||||
|
)
|
||||||
|
|
||||||
|
// FromCommand turns a ffcli.Command into markdown
|
||||||
|
func FromCommand(cmd *ffcli.Command) (string, error) {
|
||||||
|
return fromCommand(cmd, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromFlagSet turns a flag.FlagSet into markdown
|
||||||
|
func FromFlagSet(fs *flag.FlagSet) (string, error) {
|
||||||
|
return flagSetCommand(fs, 2).Markdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromCommand(cmd *ffcli.Command, section int) (string, error) {
|
||||||
|
c := flagSetCommand(cmd.FlagSet, section)
|
||||||
|
c.Name = cmd.Name
|
||||||
|
c.Usage = fmt.Sprintf("%s [FLAGS] [ARGS...]", cmd.Name)
|
||||||
|
if cmd.ShortUsage != "" {
|
||||||
|
c.Usage = cmd.ShortUsage
|
||||||
|
}
|
||||||
|
c.Description = c.Name
|
||||||
|
if cmd.ShortHelp != "" {
|
||||||
|
c.Description = cmd.ShortHelp
|
||||||
|
}
|
||||||
|
if cmd.LongHelp != "" {
|
||||||
|
c.Description = cmd.LongHelp
|
||||||
|
}
|
||||||
|
// Only top-level gets a tree
|
||||||
|
if section == 1 {
|
||||||
|
c.Tree = Tree(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var md strings.Builder
|
||||||
|
s, err := c.Markdown()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
md.WriteString(s)
|
||||||
|
md.WriteString("\n\n-----\n\n")
|
||||||
|
for _, sub := range cmd.Subcommands {
|
||||||
|
s, err = fromCommand(sub, section+1)
|
||||||
|
md.WriteString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return md.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagSetCommand(fs *flag.FlagSet, section int) command {
|
||||||
|
a := command{
|
||||||
|
Section: section,
|
||||||
|
Name: fs.Name(),
|
||||||
|
Description: "",
|
||||||
|
Usage: fmt.Sprintf("%s [FLAGS] [ARGS...]", fs.Name()),
|
||||||
|
Flags: []appFlag{
|
||||||
|
{
|
||||||
|
Name: "help",
|
||||||
|
Usage: "Show help",
|
||||||
|
IsBool: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fs.VisitAll(func(f *flag.Flag) {
|
||||||
|
_, isBool := f.Value.(boolFlag)
|
||||||
|
def := f.DefValue
|
||||||
|
if isZeroValue(f, def) {
|
||||||
|
def = ""
|
||||||
|
}
|
||||||
|
a.Flags = append(a.Flags, appFlag{
|
||||||
|
Name: f.Name,
|
||||||
|
Usage: f.Usage,
|
||||||
|
Default: def,
|
||||||
|
IsBool: isBool,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
sort.Slice(a.Flags, func(i, j int) bool {
|
||||||
|
return a.Flags[i].Name < a.Flags[j].Name
|
||||||
|
})
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
type command struct {
|
||||||
|
Section int
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Usage string
|
||||||
|
Flags []appFlag
|
||||||
|
Tree string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c command) Header() string {
|
||||||
|
return strings.Repeat("#", c.Section)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c command) Markdown() (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := Template.Execute(&buf, c); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type appFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
Default string
|
||||||
|
IsBool bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// From stdlib
|
||||||
|
|
||||||
|
func isZeroValue(f *flag.Flag, value string) bool {
|
||||||
|
// Build a zero value of the flag's Value type, and see if the
|
||||||
|
// result of calling its String method equals the value passed in.
|
||||||
|
// This works unless the Value type is itself an interface type.
|
||||||
|
typ := reflect.TypeOf(f.Value)
|
||||||
|
var z reflect.Value
|
||||||
|
if typ.Kind() == reflect.Ptr {
|
||||||
|
z = reflect.New(typ.Elem())
|
||||||
|
} else {
|
||||||
|
z = reflect.Zero(typ)
|
||||||
|
}
|
||||||
|
return value == z.Interface().(flag.Value).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type boolFlag interface {
|
||||||
|
flag.Value
|
||||||
|
IsBoolFlag() bool
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{{.Header}} {{.Name}}
|
||||||
|
{{- if .Description}}
|
||||||
|
|
||||||
|
{{.Description}}
|
||||||
|
{{- end}}
|
||||||
|
{{if .Tree}}
|
||||||
|
```
|
||||||
|
{{.Tree}}
|
||||||
|
```
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
```{{range $flag := .Flags}}
|
||||||
|
[--{{$flag.Name}}]{{if not $flag.IsBool}}=[value]{{end}}
|
||||||
|
{{- end}}
|
||||||
|
```
|
||||||
|
{{- if .Usage}}
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
{{.Usage}}
|
||||||
|
```
|
||||||
|
{{- end -}}
|
||||||
|
{{range $flag := .Flags}}
|
||||||
|
|
||||||
|
**--{{$flag.Name}}**{{if not $flag.IsBool}}=""{{end}}: {{$flag.Usage}}{{if $flag.Default}} (default: `{{$flag.Default}}`){{end}}
|
||||||
|
{{end}}
|
|
@ -0,0 +1 @@
|
||||||
|
package ffmd
|
|
@ -0,0 +1,8 @@
|
||||||
|
module go.jolheiser.com/ffmd
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/matryer/is v1.4.0
|
||||||
|
github.com/peterbourgon/ff/v3 v3.1.2
|
||||||
|
)
|
|
@ -0,0 +1,9 @@
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||||
|
github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM=
|
||||||
|
github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,96 @@
|
||||||
|
package ffmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tree returns a tree-representation of a ffcli.Command
|
||||||
|
func Tree(cmd *ffcli.Command) string {
|
||||||
|
t := tree{}
|
||||||
|
for _, path := range commandPaths(cmd, "") {
|
||||||
|
t.add(path)
|
||||||
|
}
|
||||||
|
s := t.String(true, "")
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandPaths(cmd *ffcli.Command, path string) []string {
|
||||||
|
root := fmt.Sprintf("%s%s", path, cmd.Name)
|
||||||
|
s := []string{root}
|
||||||
|
for _, sub := range cmd.Subcommands {
|
||||||
|
s = append(s, commandPaths(sub, root+"/")...)
|
||||||
|
}
|
||||||
|
sort.SliceStable(s, func(i, j int) bool {
|
||||||
|
return s[i] < s[j]
|
||||||
|
})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type tree map[string]tree
|
||||||
|
|
||||||
|
func (t tree) add(path string) {
|
||||||
|
t.addParts(strings.Split(path, "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tree) addParts(parts []string) {
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next, ok := t[parts[0]]
|
||||||
|
if !ok {
|
||||||
|
next = tree{}
|
||||||
|
t[parts[0]] = next
|
||||||
|
}
|
||||||
|
next.addParts(parts[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tree) keys() []string {
|
||||||
|
k := make([]string, 0, len(t))
|
||||||
|
for key, _ := range t {
|
||||||
|
k = append(k, key)
|
||||||
|
}
|
||||||
|
sort.Strings(k)
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tree) String(root bool, padding string) string {
|
||||||
|
var s strings.Builder
|
||||||
|
index := 0
|
||||||
|
for _, k := range t.keys() {
|
||||||
|
v := t[k]
|
||||||
|
s.WriteString(fmt.Sprintf("%s%s\n", padding+pipePad(root, pipe(index, len(t))), k))
|
||||||
|
s.WriteString(v.String(false, padding+pipePad(root, outerPipe(index, len(t)))))
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipe(index int, len int) string {
|
||||||
|
switch {
|
||||||
|
case index+1 == len:
|
||||||
|
return "└─"
|
||||||
|
case index+1 > len:
|
||||||
|
return " "
|
||||||
|
default:
|
||||||
|
return "├─"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func outerPipe(index int, len int) string {
|
||||||
|
switch {
|
||||||
|
case index+1 == len:
|
||||||
|
return " "
|
||||||
|
default:
|
||||||
|
return "│ "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipePad(root bool, box string) string {
|
||||||
|
if root {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return box + " "
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package ffmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTree(t *testing.T) {
|
||||||
|
is := is.NewRelaxed(t)
|
||||||
|
|
||||||
|
treeSingle := Tree(cmdSingle)
|
||||||
|
is.Equal(treeSingle, singleTree) // single command tree
|
||||||
|
|
||||||
|
treeSub := Tree(cmdRoot)
|
||||||
|
is.Equal(treeSub, subTree) // multi-command tree
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cmdSingle = &ffcli.Command{
|
||||||
|
Name: "myapp",
|
||||||
|
}
|
||||||
|
sub1 = &ffcli.Command{
|
||||||
|
Name: "sub1",
|
||||||
|
}
|
||||||
|
sub4 = &ffcli.Command{
|
||||||
|
Name: "sub4",
|
||||||
|
}
|
||||||
|
sub5 = &ffcli.Command{
|
||||||
|
Name: "sub5",
|
||||||
|
}
|
||||||
|
sub6 = &ffcli.Command{
|
||||||
|
Name: "sub6",
|
||||||
|
}
|
||||||
|
sub3 = &ffcli.Command{
|
||||||
|
Name: "sub3",
|
||||||
|
Subcommands: []*ffcli.Command{sub4},
|
||||||
|
}
|
||||||
|
sub2 = &ffcli.Command{
|
||||||
|
Name: "sub2",
|
||||||
|
Subcommands: []*ffcli.Command{sub3, sub5},
|
||||||
|
}
|
||||||
|
cmdRoot = &ffcli.Command{
|
||||||
|
Name: "myapp",
|
||||||
|
Subcommands: []*ffcli.Command{sub1, sub2, sub6},
|
||||||
|
}
|
||||||
|
|
||||||
|
singleTree = `myapp`
|
||||||
|
subTree = `myapp
|
||||||
|
├─ sub1
|
||||||
|
├─ sub2
|
||||||
|
│ ├─ sub3
|
||||||
|
│ │ └─ sub4
|
||||||
|
│ └─ sub5
|
||||||
|
└─ sub6`
|
||||||
|
)
|
Loading…
Reference in New Issue