Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ type Command interface {
Usage() string
}

// ExampleProvider is an interface for commands that provide usage examples.
type ExampleProvider interface {
// Examples returns the examples for this command.
Examples() []Example
}

// Example represents a usage example for a command.
type Example struct {
Name string // Short description of what the example demonstrates
Body string // The example command line or code
}

// OutputFormatter is an interface for commands that can specify their output format
type OutputFormatter interface {
// OutputFormat returns the output format for this command
Expand All @@ -65,6 +77,7 @@ type funcCommand struct {
flags *FlagSet
handler func(fs *FlagSet, args []string) error
usage string
examples []Example
outputFormat OutputFormat
}

Expand All @@ -78,6 +91,13 @@ func WithUsage(usage string) CommandOption {
}
}

// WithExamples sets the usage examples for the command.
func WithExamples(examples ...Example) CommandOption {
return func(c *funcCommand) {
c.examples = examples
}
}

// WithOutputFormat sets the output format for the command
func WithOutputFormat(format OutputFormat) CommandOption {
return func(c *funcCommand) {
Expand Down Expand Up @@ -120,6 +140,11 @@ func (c *funcCommand) Usage() string {
return c.usage
}

// Examples returns the usage examples for this command.
func (c *funcCommand) Examples() []Example {
return c.examples
}

// OutputFormat returns the output format for this command
func (c *funcCommand) OutputFormat() OutputFormat {
return c.outputFormat
Expand Down
1 change: 1 addition & 0 deletions dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1698,3 +1698,4 @@ func TestDispatcherErrShowHelp(t *testing.T) {
assert.Contains(t, output, "Usage: myapp test")
})
}

170 changes: 170 additions & 0 deletions help_doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package mflags

import "encoding/json"

// ChoiceProvider is an interface for Value types that expose a set of valid choices.
// The existing choiceValue type already satisfies this interface.
type ChoiceProvider interface {
Choices() []string
}

// HelpDocument is the top-level structure for JSON help output from a Dispatcher.
type HelpDocument struct {
Name string `json:"name"`
Commands []CommandDoc `json:"commands"`
}

// CommandDoc describes a single command in the help document.
type CommandDoc struct {
Path string `json:"path"`
Usage string `json:"usage"`
Flags []FlagDoc `json:"flags"`
FlagGroups []string `json:"flagGroups"`
PositionalArgs []PositionalDoc `json:"positionalArgs,omitempty"`
AcceptsRestArgs bool `json:"acceptsRestArgs,omitempty"`
Examples []ExampleDoc `json:"examples,omitempty"`
Subcommands []CommandDoc `json:"subcommands,omitempty"`
}

// ExampleDoc describes a usage example in the help document.
type ExampleDoc struct {
Name string `json:"name"`
Body string `json:"body"`
}

// FlagDoc describes a single flag in the help document.
type FlagDoc struct {
Name string `json:"name"`
Short string `json:"short"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
Usage string `json:"usage"`
Group string `json:"group,omitempty"`
IsBool bool `json:"isBool,omitempty"`
Choices []string `json:"choices,omitempty"`
}

// PositionalDoc describes a positional argument in the help document.
type PositionalDoc struct {
Name string `json:"name"`
Usage string `json:"usage"`
Type string `json:"type"`
}

// FlagSetDoc describes a standalone FlagSet's help information.
type FlagSetDoc struct {
Name string `json:"name"`
Flags []FlagDoc `json:"flags"`
FlagGroups []string `json:"flagGroups"`
PositionalArgs []PositionalDoc `json:"positionalArgs"`
AcceptsRestArgs bool `json:"acceptsRestArgs"`
}

// HelpDoc generates a structured help document for a FlagSet.
func (f *FlagSet) HelpDoc() *FlagSetDoc {
doc := &FlagSetDoc{
Name: f.name,
Flags: []FlagDoc{},
FlagGroups: []string{},
PositionalArgs: []PositionalDoc{},
AcceptsRestArgs: f.restField != nil,
}

f.VisitAll(func(flag *Flag) {
fd := FlagDoc{
Name: flag.Name,
Type: flag.Value.Type(),
Default: flag.DefValue,
Usage: flag.Usage,
Group: flag.Group,
IsBool: flag.Value.IsBool(),
Choices: []string{},
}
if flag.Short != 0 {
fd.Short = string(flag.Short)
}
if cp, ok := flag.Value.(ChoiceProvider); ok {
fd.Choices = cp.Choices()
}
doc.Flags = append(doc.Flags, fd)
})

if len(f.groupOrder) > 0 {
doc.FlagGroups = append(doc.FlagGroups, f.groupOrder...)
}

for _, pf := range f.GetPositionalFields() {
doc.PositionalArgs = append(doc.PositionalArgs, PositionalDoc{
Name: pf.Name,
Usage: pf.Usage,
Type: pf.Type.String(),
})
}

return doc
}

// HelpJSON returns the FlagSet's help document as indented JSON.
func (f *FlagSet) HelpJSON() ([]byte, error) {
return json.MarshalIndent(f.HelpDoc(), "", " ")
}

// HelpDoc generates a structured help document for the entire Dispatcher.
func (d *Dispatcher) HelpDoc() *HelpDocument {
doc := &HelpDocument{
Name: d.name,
Commands: d.buildCommandDocs(""),
}
return doc
}

// HelpJSON returns the Dispatcher's help document as indented JSON.
func (d *Dispatcher) HelpJSON() ([]byte, error) {
return json.MarshalIndent(d.HelpDoc(), "", " ")
}

// buildCommandDocs recursively builds CommandDoc entries for direct children of parentPath.
func (d *Dispatcher) buildCommandDocs(parentPath string) []CommandDoc {
children := d.getDirectChildren(parentPath)
docs := make([]CommandDoc, 0, len(children))

for _, child := range children {
cmd := CommandDoc{
Path: child.Path,
Usage: child.Usage,
Flags: []FlagDoc{},
FlagGroups: []string{},
PositionalArgs: []PositionalDoc{},
AcceptsRestArgs: false,
Subcommands: []CommandDoc{},
}

if child.IsEntry {
entry := d.commands[child.Path]
if entry != nil && entry.Command != nil {
fs := entry.Command.FlagSet()
if fs != nil {
fsDoc := fs.HelpDoc()
cmd.Flags = fsDoc.Flags
cmd.FlagGroups = fsDoc.FlagGroups
cmd.PositionalArgs = fsDoc.PositionalArgs
cmd.AcceptsRestArgs = fsDoc.AcceptsRestArgs
}
if ep, ok := entry.Command.(ExampleProvider); ok {
for _, ex := range ep.Examples() {
cmd.Examples = append(cmd.Examples, ExampleDoc{
Name: ex.Name,
Body: ex.Body,
})
}
}
}
}

cmd.Subcommands = d.buildCommandDocs(child.Path)

docs = append(docs, cmd)
}

return docs
}
Loading