1003 lines
21 KiB
Go
1003 lines
21 KiB
Go
package wand
|
|
|
|
import (
|
|
"code.squareroundforest.org/arpio/bind"
|
|
"code.squareroundforest.org/arpio/docreflect"
|
|
. "code.squareroundforest.org/arpio/textfmt"
|
|
"fmt"
|
|
"github.com/iancoleman/strcase"
|
|
"golang.org/x/term"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultHelpWidth = 72
|
|
minPrintWidth = 42
|
|
maxPrintWidth = 360
|
|
markdownWrapWidth = 112
|
|
)
|
|
|
|
const (
|
|
noOptionHints = iota
|
|
commandSpecificHints
|
|
allRelevantHints
|
|
)
|
|
|
|
var sentenceDelimiter = regexp.MustCompile("[.?!]")
|
|
|
|
func help(cmd Cmd) Cmd {
|
|
h := Cmd{
|
|
name: "help",
|
|
helpFor: &cmd,
|
|
shortForms: cmd.shortForms,
|
|
}
|
|
|
|
cmd.subcommands = append(cmd.subcommands, h)
|
|
return cmd
|
|
}
|
|
|
|
func insertHelp(cmd Cmd) Cmd {
|
|
var hasHelpCmd bool
|
|
for i, sc := range cmd.subcommands {
|
|
if sc.name == "help" {
|
|
hasHelpCmd = true
|
|
continue
|
|
}
|
|
|
|
cmd.subcommands[i] = insertHelp(sc)
|
|
}
|
|
|
|
if hasHelpCmd || cmd.version != "" {
|
|
return cmd
|
|
}
|
|
|
|
return help(cmd)
|
|
}
|
|
|
|
func hasHelpSubcommand(cmd Cmd) bool {
|
|
for _, sc := range cmd.subcommands {
|
|
if sc.helpFor != nil {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func hasCustomHelpOption(cmd Cmd) bool {
|
|
if cmd.impl == nil {
|
|
return false
|
|
}
|
|
|
|
mf := mapFields(cmd.impl)
|
|
_, has := mf["help"]
|
|
return has
|
|
}
|
|
|
|
func suggestHelp(out io.Writer, cmd Cmd, fullCommand []string) {
|
|
if hasHelpSubcommand(cmd) {
|
|
fmt.Fprintf(out, "Show help:\n%s help\n", strings.Join(fullCommand, " "))
|
|
return
|
|
}
|
|
|
|
if !hasCustomHelpOption(cmd) {
|
|
fmt.Fprintf(out, "Show help:\n%s --help\n", strings.Join(fullCommand, " "))
|
|
return
|
|
}
|
|
}
|
|
|
|
func allNonHelpOptions(cmd Cmd) []bind.Field {
|
|
var options []bind.Field
|
|
if cmd.impl != nil {
|
|
f := fields(cmd.impl)
|
|
for _, fi := range f {
|
|
options = append(options, fi)
|
|
}
|
|
}
|
|
|
|
for _, sc := range cmd.subcommands {
|
|
options = append(options, allNonHelpOptions(sc)...)
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
func allConfigFiles(conf Config) []Config {
|
|
if conf.file != nil || conf.fromOption {
|
|
return []Config{conf}
|
|
}
|
|
|
|
var files []Config
|
|
for _, c := range conf.merge {
|
|
files = append(files, allConfigFiles(c)...)
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
func functionParams(v any, indices []int) ([]string, []string) {
|
|
var names []string
|
|
r := reflect.ValueOf(v)
|
|
allNames := docreflect.FunctionParams(r)
|
|
if len(allNames) == r.Type().NumIn() {
|
|
for _, i := range indices {
|
|
names = append(names, allNames[i])
|
|
}
|
|
} else {
|
|
names = make([]string, len(indices))
|
|
}
|
|
|
|
for i := range names {
|
|
names[i] = strings.TrimSpace(names[i])
|
|
if names[i] == "" {
|
|
names[i] = fmt.Sprintf("arg%d", i+1)
|
|
}
|
|
}
|
|
|
|
var types []string
|
|
for _, i := range indices {
|
|
t := r.Type().In(i)
|
|
types = append(types, scalarTypeStringOf(t))
|
|
}
|
|
|
|
return names, types
|
|
}
|
|
|
|
func paragraphs(text string) []string {
|
|
l := strings.Split(text, "\n")
|
|
for i := range l {
|
|
l[i] = strings.TrimSpace(l[i])
|
|
}
|
|
|
|
var (
|
|
pl []string
|
|
p []string
|
|
)
|
|
|
|
for _, li := range l {
|
|
if li == "" && len(pl) > 0 {
|
|
p = append(p, strings.Join(pl, " "))
|
|
pl = nil
|
|
continue
|
|
}
|
|
|
|
if li == "" {
|
|
continue
|
|
}
|
|
|
|
pl = append(pl, li)
|
|
}
|
|
|
|
if len(pl) > 0 {
|
|
p = append(p, strings.Join(pl, " "))
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
func firstSentence(text string) string {
|
|
s := sentenceDelimiter.Split(text, 2)
|
|
s[0] = strings.TrimSpace(s[0])
|
|
return s[0]
|
|
}
|
|
|
|
func wrapWidth() int {
|
|
width := defaultHelpWidth
|
|
fd := int(os.Stdin.Fd())
|
|
if !term.IsTerminal(fd) {
|
|
return width
|
|
}
|
|
|
|
w, _, err := term.GetSize(fd)
|
|
if err != nil {
|
|
return width
|
|
}
|
|
|
|
width = w - 8
|
|
if width < minPrintWidth {
|
|
width = minPrintWidth
|
|
}
|
|
|
|
if width > maxPrintWidth {
|
|
width = maxPrintWidth
|
|
}
|
|
|
|
return width
|
|
}
|
|
|
|
func implementationCommand(cmd Cmd) Cmd {
|
|
if cmd.impl != nil {
|
|
return cmd
|
|
}
|
|
|
|
for _, subCmd := range cmd.subcommands {
|
|
if subCmd.isDefault {
|
|
return subCmd
|
|
}
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func docCommandNameText(cmd Cmd, fullCommand []string) []string {
|
|
command := strings.Join(fullCommand, " ")
|
|
fdoc := docreflect.Function(reflect.ValueOf(cmd.impl))
|
|
fdoc = strings.TrimSpace(fdoc)
|
|
fdocp := paragraphs(fdoc)
|
|
if len(fdocp) == 0 {
|
|
return []string{command}
|
|
}
|
|
|
|
return []string{command, "-", firstSentence(fdocp[0])}
|
|
}
|
|
|
|
func docCommandName(cmd Cmd, fullCommand []string) Entry {
|
|
s := docCommandNameText(cmd, fullCommand)
|
|
txt := make([]Txt, len(s))
|
|
for i := range s {
|
|
txt[i] = Text(s[i])
|
|
}
|
|
|
|
return Paragraph(Cat(txt...))
|
|
}
|
|
|
|
func docSynopsis(cmd Cmd, fullCommand []string, titleLevel, wrapWidth int) []Entry {
|
|
implCmd := implementationCommand(cmd)
|
|
command := strings.Join(fullCommand, " ")
|
|
synopsisTitle := Title(titleLevel, "Synopsis:")
|
|
if implCmd.impl == nil {
|
|
synopsis := Syntax(Choice(Sequence(Symbol(command), Required(Symbol("subcommand")))))
|
|
return []Entry{synopsisTitle, Indent(synopsis, 4, 0)}
|
|
}
|
|
|
|
pi := positionalIndices(implCmd.impl)
|
|
names, types := functionParams(implCmd.impl, pi)
|
|
if len(names) == 0 {
|
|
synopsis := Syntax(
|
|
Choice(
|
|
Sequence(Symbol(command), ZeroOrMore(Symbol("options"))),
|
|
Sequence(Symbol(command), Required(Symbol("subcommand"))),
|
|
),
|
|
)
|
|
|
|
return []Entry{synopsisTitle, Indent(synopsis, 4, 0)}
|
|
}
|
|
|
|
var args []SyntaxItem
|
|
_, variadic := positional(implCmd.impl)
|
|
for i := range names[:len(names)-1] {
|
|
args = append(args, Required(Sequence(Symbol(names[i]), Symbol(types[i]))))
|
|
}
|
|
|
|
lastArity := Required
|
|
switch {
|
|
case variadic && implCmd.minPositional >= len(pi):
|
|
lastArity = OneOrMore
|
|
case variadic:
|
|
lastArity = ZeroOrMore
|
|
}
|
|
|
|
last := len(names) - 1
|
|
args = append(args, lastArity(Sequence(Symbol(names[last]), Symbol(types[last]))))
|
|
|
|
var argCountHint Entry
|
|
needsArgCountHint := implCmd.impl != nil && (implCmd.minPositional > 0 || implCmd.maxPositional > 0)
|
|
if needsArgCountHint {
|
|
var hint string
|
|
switch {
|
|
case implCmd.minPositional > 0 && implCmd.maxPositional > 0:
|
|
hint = fmt.Sprintf(
|
|
"Expecting min %d and max %d total number of arguments.",
|
|
implCmd.minPositional,
|
|
implCmd.maxPositional,
|
|
)
|
|
case implCmd.minPositional > 0:
|
|
hint = fmt.Sprintf(
|
|
"Expecting min %d total number of arguments.",
|
|
implCmd.minPositional,
|
|
)
|
|
case implCmd.maxPositional > 0:
|
|
hint = fmt.Sprintf(
|
|
"Expecting max %d total number of arguments.",
|
|
implCmd.maxPositional,
|
|
)
|
|
}
|
|
|
|
argCountHint = Paragraph(Text(hint))
|
|
}
|
|
|
|
if len(args) > 0 {
|
|
args = append([]SyntaxItem{Optional(Symbol("--"))}, args...)
|
|
}
|
|
|
|
synopsis := Syntax(
|
|
Choice(
|
|
Sequence(append([]SyntaxItem{Symbol(command), ZeroOrMore(Symbol("options"))}, args...)...),
|
|
Sequence(Symbol(command), Required(Symbol("subcommand"))),
|
|
),
|
|
)
|
|
|
|
if !needsArgCountHint {
|
|
return []Entry{synopsisTitle, Indent(synopsis, 4, 0)}
|
|
}
|
|
|
|
return []Entry{synopsisTitle, Indent(synopsis, 4, 0), Wrap(argCountHint, wrapWidth)}
|
|
}
|
|
|
|
func docs(cmd Cmd) []Entry {
|
|
implCmd := implementationCommand(cmd)
|
|
if implCmd.impl == nil {
|
|
return nil
|
|
}
|
|
|
|
fdoc := docreflect.Function(reflect.ValueOf(implCmd.impl))
|
|
fdoc = strings.TrimSpace(fdoc)
|
|
fdocp := paragraphs(fdoc)
|
|
if len(fdocp) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var e []Entry
|
|
for _, p := range fdocp {
|
|
e = append(e, Paragraph(Text(p)))
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func docOptions(cmd Cmd, conf Config, titleLevel, wrapWidth, hintStyle int) []Entry {
|
|
implCmd := implementationCommand(cmd)
|
|
optionsTitle := Title(titleLevel, "Options:")
|
|
fields := make(map[string][]bind.Field)
|
|
docs := make(map[string]string)
|
|
structs := structParameters(implCmd.impl)
|
|
for _, s := range structs {
|
|
f := structFields(s)
|
|
for _, fi := range f {
|
|
fields[fi.Name()] = append(fields[fi.Name()], fi)
|
|
if d := docs[fi.Name()]; d == "" {
|
|
docs[fi.Name()] = docreflect.Field(s, fi.Path()...)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(fields) == 0 {
|
|
var optionItems []DefinitionItem
|
|
if _, ok := fields["config"]; !ok && hasConfigFromOption(conf) {
|
|
optionItems = append(
|
|
optionItems,
|
|
Definition(Text("--config"), Text("Configuration file."), NoBullet()),
|
|
)
|
|
}
|
|
|
|
optionItems = append(
|
|
optionItems,
|
|
Definition(
|
|
Text("--help"),
|
|
Text("Show help."),
|
|
NoBullet(),
|
|
),
|
|
)
|
|
|
|
return []Entry{
|
|
optionsTitle,
|
|
Wrap(Indent(DefinitionList(optionItems...), 4, 0), wrapWidth),
|
|
}
|
|
}
|
|
|
|
var (
|
|
names []string
|
|
optionItems []DefinitionItem
|
|
hasAcceptsMultiple, hasBoolOptions, hasGrouping bool
|
|
)
|
|
|
|
for n := range fields {
|
|
names = append(names, n)
|
|
}
|
|
|
|
sort.Strings(names)
|
|
s2l := shortFormsToLong(implCmd.shortForms)
|
|
l2s := longFormsToShort(implCmd.shortForms)
|
|
for _, name := range names {
|
|
var def string
|
|
f := fields[name]
|
|
sf := l2s[name]
|
|
if len(sf) == 0 {
|
|
def = fmt.Sprintf("--%s %s", name, scalarTypeString(f[0].Type()))
|
|
} else {
|
|
sfs := make([]string, len(sf))
|
|
copy(sfs, sf)
|
|
for i := range sfs {
|
|
sfs[i] = fmt.Sprintf("-%s", sfs[i])
|
|
}
|
|
|
|
def = fmt.Sprintf(
|
|
"--%s, %s %s",
|
|
name,
|
|
strings.Join(sfs, ", "),
|
|
scalarTypeString(f[0].Type()),
|
|
)
|
|
}
|
|
|
|
for _, fi := range f {
|
|
if fi.List() {
|
|
def = fmt.Sprintf("%s [*]", def)
|
|
hasAcceptsMultiple = true
|
|
}
|
|
}
|
|
|
|
if f[0].Type() == bind.Bool {
|
|
hasBoolOptions = true
|
|
if len(s2l) > 1 && len(sf) > 0 {
|
|
hasGrouping = true
|
|
}
|
|
}
|
|
|
|
optionItems = append(
|
|
optionItems,
|
|
Definition(Text(def), Text(docs[name]), NoBullet()),
|
|
)
|
|
}
|
|
|
|
if _, ok := fields["config"]; !ok && hasConfigFromOption(conf) {
|
|
optionItems = append(
|
|
optionItems,
|
|
Definition(Text("--config"), Text("Configuration file."), NoBullet()),
|
|
)
|
|
}
|
|
|
|
var (
|
|
helpShortForms []string
|
|
helpShortFormsString string
|
|
)
|
|
|
|
_, hasHelpOption := fields["help"]
|
|
if !hasHelpOption {
|
|
hsf := make([]string, len(l2s["help"]))
|
|
copy(hsf, l2s["help"])
|
|
sort.Strings(hsf)
|
|
helpShortForms = append(helpShortForms, hsf...)
|
|
}
|
|
|
|
if len(helpShortForms) > 0 {
|
|
for i := range helpShortForms {
|
|
helpShortForms[i] = fmt.Sprintf("-%s", helpShortForms[i])
|
|
}
|
|
|
|
helpShortFormsString = strings.Join(helpShortForms, ", ")
|
|
}
|
|
|
|
if helpShortFormsString != "" {
|
|
optionItems = append(
|
|
optionItems,
|
|
Definition(
|
|
Text(fmt.Sprintf("--help, %s", helpShortFormsString)),
|
|
Text("Show help."), NoBullet(),
|
|
),
|
|
)
|
|
} else if !hasHelpOption {
|
|
optionItems = append(
|
|
optionItems,
|
|
Definition(Text("--help"), Text("Show help."), NoBullet()),
|
|
)
|
|
}
|
|
|
|
if hintStyle == noOptionHints {
|
|
return []Entry{
|
|
optionsTitle,
|
|
Wrap(Indent(DefinitionList(optionItems...), 4, 0), wrapWidth),
|
|
}
|
|
}
|
|
|
|
switch hintStyle {
|
|
case allRelevantHints:
|
|
var numShortForm int
|
|
allOptions := allNonHelpOptions(cmd)
|
|
for _, o := range allOptions {
|
|
hasAcceptsMultiple = hasAcceptsMultiple || o.List()
|
|
hasBoolOptions = hasBoolOptions || o.Type() == bind.Bool
|
|
if _, hasShortForm := l2s[o.Name()]; hasShortForm {
|
|
numShortForm++
|
|
}
|
|
|
|
hasGrouping = hasGrouping || o.Type() == bind.Bool && numShortForm > 1
|
|
}
|
|
}
|
|
|
|
var hints []string
|
|
if hasAcceptsMultiple {
|
|
hints = append(hints, docListOptions)
|
|
}
|
|
|
|
if hasBoolOptions {
|
|
hints = append(hints, docBoolOptions)
|
|
}
|
|
|
|
if hasGrouping {
|
|
hints = append(hints, docOptionGrouping)
|
|
}
|
|
|
|
hints = append(hints, docOptionValues)
|
|
|
|
if len(hints) == 1 {
|
|
return []Entry{
|
|
optionsTitle,
|
|
Wrap(Indent(DefinitionList(optionItems...), 4, 0), wrapWidth),
|
|
Wrap(Paragraph(Text(hints[0])), wrapWidth),
|
|
}
|
|
}
|
|
|
|
items := make([]ListItem, len(hints))
|
|
for i := range hints {
|
|
items[i] = Item(Text(hints[i]))
|
|
}
|
|
|
|
return []Entry{
|
|
optionsTitle,
|
|
Wrap(Indent(DefinitionList(optionItems...), 4, 0), wrapWidth),
|
|
Wrap(Paragraph(Text("Hints:")), wrapWidth),
|
|
Wrap(List(items...), wrapWidth),
|
|
}
|
|
}
|
|
|
|
func docListSubcommands(cmd Cmd, fullCommand []string, level, wrapWidth int) []Entry {
|
|
subcommandsTitle := Title(level+1, "Subcommands:")
|
|
|
|
var (
|
|
items []DefinitionItem
|
|
hasNonHelp bool
|
|
)
|
|
|
|
scs := make([]Cmd, len(cmd.subcommands))
|
|
copy(scs, cmd.subcommands)
|
|
sort.Slice(scs, func(i, j int) bool {
|
|
return scs[i].name < scs[j].name
|
|
})
|
|
|
|
for _, sc := range scs {
|
|
var description string
|
|
name := sc.name
|
|
if sc.helpFor != nil {
|
|
description = "Show help."
|
|
} else if sc.version != "" {
|
|
description = "Show version information."
|
|
} else {
|
|
if sc.isDefault {
|
|
name = fmt.Sprintf("%s (default)", name)
|
|
}
|
|
|
|
hasNonHelp = true
|
|
description = docreflect.Function(reflect.ValueOf(sc.impl))
|
|
description = strings.TrimSpace(description)
|
|
descriptionP := paragraphs(description)
|
|
if len(descriptionP) > 0 {
|
|
description = firstSentence(descriptionP[0])
|
|
}
|
|
}
|
|
|
|
items = append(items, Definition(Text(name), Text(description), NoBullet()))
|
|
}
|
|
|
|
if hasNonHelp {
|
|
command := strings.Join(fullCommand, " ")
|
|
hint := Paragraph(Text(fmt.Sprintf(docSubcommandHelpFmt, command, command)))
|
|
return []Entry{
|
|
subcommandsTitle,
|
|
Wrap(Indent(DefinitionList(items...), 4, 0), wrapWidth),
|
|
Wrap(hint, wrapWidth),
|
|
}
|
|
}
|
|
|
|
return []Entry{
|
|
subcommandsTitle,
|
|
Wrap(Indent(DefinitionList(items...), 4, 0), wrapWidth),
|
|
}
|
|
}
|
|
|
|
func docFullSubcommands(cmd Cmd, conf Config, level int) []Entry {
|
|
var e []Entry
|
|
e = append(
|
|
e,
|
|
Title(level+1, "Subcommands:"),
|
|
Paragraph(Text(docSubcommandHint)),
|
|
)
|
|
|
|
type subcommandDef struct {
|
|
fullCommand []string
|
|
command Cmd
|
|
}
|
|
|
|
var (
|
|
allSubcommands []subcommandDef
|
|
collectSubcommands func(cmd Cmd) []subcommandDef
|
|
)
|
|
|
|
collectSubcommands = func(cmd Cmd) []subcommandDef {
|
|
var defs []subcommandDef
|
|
for _, sc := range cmd.subcommands {
|
|
if sc.impl != nil || sc.version != "" {
|
|
defs = append(defs, subcommandDef{fullCommand: []string{cmd.name, sc.name}, command: sc})
|
|
}
|
|
|
|
scDefs := collectSubcommands(sc)
|
|
for i := range scDefs {
|
|
scDefs[i].fullCommand = append([]string{cmd.name}, scDefs[i].fullCommand...)
|
|
}
|
|
|
|
defs = append(defs, scDefs...)
|
|
}
|
|
|
|
return defs
|
|
}
|
|
|
|
allSubcommands = collectSubcommands(cmd)
|
|
for _, sc := range allSubcommands {
|
|
command := strings.Join(sc.fullCommand, " ")
|
|
if sc.command.isDefault {
|
|
command = fmt.Sprintf("%s (default)", command)
|
|
}
|
|
|
|
e = append(e, Indent(Title(level+2, command), 2, 0))
|
|
if sc.command.version != "" {
|
|
e = append(e, Indent(Paragraph(Text("Show version.")), 4, 0))
|
|
continue
|
|
}
|
|
|
|
synopsis := docSynopsis(sc.command, sc.fullCommand, 3, 0)
|
|
for i := range synopsis {
|
|
synopsis[i] = Indent(synopsis[i], 4, 0)
|
|
}
|
|
|
|
e = append(e, synopsis...)
|
|
if docs := docs(sc.command); len(docs) > 0 {
|
|
e = append(e, Indent(Title(level+3, "Description:"), 4, 0))
|
|
for i := range docs {
|
|
docs[i] = Indent(docs[i], 4, 0)
|
|
}
|
|
|
|
e = append(e, docs...)
|
|
}
|
|
|
|
options := docOptions(sc.command, conf, 3, 0, noOptionHints)
|
|
for i := range options {
|
|
options[i] = Indent(options[i], 4, 0)
|
|
}
|
|
|
|
e = append(e, options...)
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func docEnv(cmd Cmd, level int) []Entry {
|
|
// env will not work if the app has a non-standard name:
|
|
if !commandNameExpression.MatchString(cmd.name) {
|
|
return nil
|
|
}
|
|
|
|
options := allNonHelpOptions(cmd)
|
|
if len(options) == 0 {
|
|
return nil
|
|
}
|
|
|
|
e := []Entry{Title(level+1, "Environment variables:")}
|
|
p := paragraphs(envDocs)
|
|
for _, pi := range p {
|
|
e = append(e, Indent(Paragraph(Text(pi)), 4, 0))
|
|
}
|
|
|
|
e = append(e, Indent(Title(level+2, "Example environment variable:"), 4, 0))
|
|
option := options[0]
|
|
name := strcase.ToScreamingSnake(fmt.Sprintf("%s-%s", cmd.name, option.Name()))
|
|
value := "42"
|
|
if option.Type() == bind.Bool {
|
|
value = "true"
|
|
}
|
|
|
|
e = append(e, Indent(CodeBlock(fmt.Sprintf("%s=%s", name, value)), 4, 0))
|
|
return e
|
|
}
|
|
|
|
func docConfig(cmd Cmd, conf Config, level int) []Entry {
|
|
options := allNonHelpOptions(cmd)
|
|
if len(options) == 0 {
|
|
return nil
|
|
}
|
|
|
|
configFiles := allConfigFiles(conf)
|
|
if len(configFiles) == 0 {
|
|
return nil
|
|
}
|
|
|
|
e := []Entry{Title(level+1, "Configuration:")}
|
|
p := paragraphs(configDocs)
|
|
for _, pi := range p {
|
|
e = append(e, Indent(Paragraph(Text(pi)), 4, 0))
|
|
}
|
|
|
|
var items []ListItem
|
|
for _, cf := range configFiles {
|
|
if cf.fromOption {
|
|
items = append(
|
|
items,
|
|
Item(Text("zero or more configuration files defined by the --config option")),
|
|
)
|
|
|
|
continue
|
|
}
|
|
|
|
text := cf.file(cmd).filename
|
|
if cf.optional {
|
|
text = fmt.Sprintf("%s (optional)", text)
|
|
}
|
|
|
|
items = append(items, Item(Text(text)))
|
|
}
|
|
|
|
e = append(
|
|
e,
|
|
Indent(Title(level+2, "Configuration files:"), 4, 0),
|
|
Indent(List(items...), 8, 0),
|
|
)
|
|
|
|
option := options[0]
|
|
name := option.Name()
|
|
value := "42"
|
|
if option.Type() == bind.Bool {
|
|
value = "true"
|
|
}
|
|
|
|
exampleCode := fmt.Sprintf(
|
|
"# Default value for --%s:\n%s = %s",
|
|
name,
|
|
strcase.ToSnake(name),
|
|
value,
|
|
)
|
|
|
|
e = append(
|
|
e,
|
|
Indent(Title(level+2, "Example configuration entry:"), 4, 0),
|
|
Indent(CodeBlock(exampleCode), 4, 0),
|
|
)
|
|
|
|
discardExample := fmt.Sprintf(
|
|
"# Discarding an inherited entry:\n%s",
|
|
strcase.ToSnake(name),
|
|
)
|
|
|
|
e = append(
|
|
e,
|
|
Indent(Title(level+2, "Example for discarding an inherited entry:"), 4, 0),
|
|
Indent(CodeBlock(discardExample), 4, 0),
|
|
)
|
|
|
|
return e
|
|
}
|
|
|
|
func helpCommandName(cmd Cmd, fullCommand []string) []Entry {
|
|
return []Entry{docCommandName(cmd, fullCommand)}
|
|
}
|
|
|
|
func helpSynopsis(cmd Cmd, fullCommand []string, width int) []Entry {
|
|
return docSynopsis(cmd, fullCommand, 1, width)
|
|
}
|
|
|
|
func helpDocs(cmd Cmd, width int) []Entry {
|
|
paragraphs := docs(cmd)
|
|
for i := range paragraphs {
|
|
paragraphs[i] = Wrap(paragraphs[i], width)
|
|
}
|
|
|
|
return paragraphs
|
|
}
|
|
|
|
func helpOptions(cmd Cmd, conf Config, width int) []Entry {
|
|
return docOptions(cmd, conf, 1, width, commandSpecificHints)
|
|
}
|
|
|
|
func helpSubcommands(cmd Cmd, fullCommand []string, width int) []Entry {
|
|
return docListSubcommands(cmd, fullCommand, 0, width)
|
|
}
|
|
|
|
func manTitle(cmd Cmd, date time.Time, version string) []Entry {
|
|
if version == "" {
|
|
for _, sc := range cmd.subcommands {
|
|
if sc.version != "" {
|
|
version = sc.version
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return []Entry{
|
|
Title(
|
|
0,
|
|
cmd.name,
|
|
ManCategory("User Commands"),
|
|
ManSection(1),
|
|
ReleaseDate(date),
|
|
ReleaseVersion(version),
|
|
),
|
|
}
|
|
}
|
|
|
|
func manCommandName(cmd Cmd) []Entry {
|
|
title := Title(1, "Name:")
|
|
name := docCommandName(cmd, []string{cmd.name})
|
|
name = Indent(name, 4, 0)
|
|
return []Entry{title, name}
|
|
}
|
|
|
|
func manSynopsis(cmd Cmd) []Entry {
|
|
return docSynopsis(cmd, []string{cmd.name}, 1, 0)
|
|
}
|
|
|
|
func manDocs(cmd Cmd) []Entry {
|
|
paragraphs := docs(cmd)
|
|
if len(paragraphs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for i := range paragraphs {
|
|
paragraphs[i] = Indent(paragraphs[i], 4, 0)
|
|
}
|
|
|
|
return append([]Entry{Title(1, "Description:")}, paragraphs...)
|
|
}
|
|
|
|
func manOptions(cmd Cmd, conf Config) []Entry {
|
|
e := docOptions(cmd, conf, 1, 0, allRelevantHints)
|
|
for i := 1; i < len(e); i++ {
|
|
e[i] = Indent(e[i], 4, 0)
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func manSubcommands(cmd Cmd, conf Config) []Entry {
|
|
var hasSubcommands bool
|
|
for _, sc := range cmd.subcommands {
|
|
if sc.helpFor != nil || sc.version != "" {
|
|
continue
|
|
}
|
|
|
|
hasSubcommands = true
|
|
break
|
|
}
|
|
|
|
if hasSubcommands {
|
|
return docFullSubcommands(cmd, conf, 0)
|
|
}
|
|
|
|
return docListSubcommands(cmd, []string{cmd.name}, 0, 0)
|
|
}
|
|
|
|
func manEnv(cmd Cmd) []Entry {
|
|
return docEnv(cmd, 0)
|
|
}
|
|
|
|
func manConfig(cmd Cmd, conf Config) []Entry {
|
|
return docConfig(cmd, conf, 0)
|
|
}
|
|
|
|
func markdownTitle(cmd Cmd, level int) []Entry {
|
|
txt := docCommandNameText(cmd, []string{cmd.name})
|
|
return []Entry{Title(level, strings.Join(txt, " "))}
|
|
}
|
|
|
|
func markdownSynopsis(cmd Cmd, level int) []Entry {
|
|
return docSynopsis(cmd, []string{cmd.name}, level+1, markdownWrapWidth)
|
|
}
|
|
|
|
func markdownDocs(cmd Cmd, level int) []Entry {
|
|
paragraphs := docs(cmd)
|
|
if len(paragraphs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for i := range paragraphs {
|
|
paragraphs[i] = Wrap(paragraphs[i], markdownWrapWidth)
|
|
}
|
|
|
|
return append([]Entry{Title(level+1, "Description:")}, paragraphs...)
|
|
}
|
|
|
|
func markdownOptions(cmd Cmd, conf Config, level int) []Entry {
|
|
e := docOptions(cmd, conf, level+1, 0, allRelevantHints)
|
|
for i := 1; i < len(e); i++ {
|
|
e[i] = Wrap(e[i], markdownWrapWidth)
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func markdownSubcommands(cmd Cmd, conf Config, level int) []Entry {
|
|
var hasSubcommands bool
|
|
for _, sc := range cmd.subcommands {
|
|
if sc.helpFor != nil || sc.version != "" {
|
|
continue
|
|
}
|
|
|
|
hasSubcommands = true
|
|
break
|
|
}
|
|
|
|
var e []Entry
|
|
if hasSubcommands {
|
|
e = docFullSubcommands(cmd, conf, level)
|
|
} else {
|
|
e = docListSubcommands(cmd, []string{cmd.name}, level, 0)
|
|
}
|
|
|
|
for i := range e {
|
|
e[i] = Wrap(e[i], markdownWrapWidth)
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func markdownEnv(cmd Cmd, level int) []Entry {
|
|
e := docEnv(cmd, level)
|
|
for i := range e {
|
|
e[i] = Wrap(e[i], markdownWrapWidth)
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func markdownConfig(cmd Cmd, conf Config, level int) []Entry {
|
|
e := docConfig(cmd, conf, level)
|
|
for i := range e {
|
|
e[i] = Wrap(e[i], markdownWrapWidth)
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func showHelp(out io.Writer, cmd Cmd, conf Config, fullCommand []string) error {
|
|
var e []Entry
|
|
width := wrapWidth()
|
|
e = append(e, helpCommandName(cmd, fullCommand)...)
|
|
e = append(e, helpSynopsis(cmd, fullCommand, width)...)
|
|
e = append(e, helpDocs(cmd, width)...)
|
|
e = append(e, helpOptions(cmd, conf, width)...)
|
|
e = append(e, helpSubcommands(cmd, fullCommand, width)...)
|
|
return Teletype(out, Doc(e...))
|
|
}
|
|
|
|
func generateMan(out io.Writer, cmd Cmd, conf Config, date time.Time, version string) error {
|
|
var e []Entry
|
|
e = append(e, manTitle(cmd, date, version)...)
|
|
e = append(e, manCommandName(cmd)...)
|
|
e = append(e, manSynopsis(cmd)...)
|
|
e = append(e, manDocs(cmd)...)
|
|
e = append(e, manOptions(cmd, conf)...)
|
|
e = append(e, manSubcommands(cmd, conf)...)
|
|
e = append(e, manEnv(cmd)...)
|
|
e = append(e, manConfig(cmd, conf)...)
|
|
return Runoff(out, Doc(e...))
|
|
}
|
|
|
|
func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
|
|
var e []Entry
|
|
e = append(e, markdownTitle(cmd, level)...)
|
|
e = append(e, markdownSynopsis(cmd, level)...)
|
|
e = append(e, markdownDocs(cmd, level)...)
|
|
e = append(e, markdownOptions(cmd, conf, level)...)
|
|
e = append(e, markdownSubcommands(cmd, conf, level)...)
|
|
e = append(e, markdownEnv(cmd, level)...)
|
|
e = append(e, markdownConfig(cmd, conf, level)...)
|
|
return Markdown(out, Doc(e...))
|
|
}
|
|
|
|
func showVersion(out io.Writer, cmd Cmd) error {
|
|
_, err := fmt.Fprintln(out, cmd.version)
|
|
return err
|
|
}
|