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
3 changes: 3 additions & 0 deletions v2/definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ type HelpStyle int
const (
HelpStyleFlat HelpStyle = iota // PrintUsage
HelpStyleGrouped // PrintUsageWithGroups
HelpStyleGroupedClean // PrintUsageWithGroups, clean (no ** markers)
HelpStyleCompact // Deduplicated, minimal
HelpStyleHierarchical // Command-focused, drill-down
HelpStyleSmart // Auto-detect based on CLI size
Expand Down Expand Up @@ -196,6 +197,7 @@ type Parser struct {
structCtx any
suggestionsFormatter SuggestionsFormatter
helpConfig HelpConfig
prettyPrintConfig *PrettyPrintConfig
helpBehavior HelpBehavior
autoHelp bool
helpFlags []string
Expand Down Expand Up @@ -246,6 +248,7 @@ type Renderer interface {
FlagName(f *Argument) string
FlagDescription(f *Argument) string
FlagUsage(f *Argument) string
PositionalUsage(f *Argument, position int) string
CommandName(c *Command) string
CommandDescription(c *Command) string
CommandUsage(c *Command) string
Expand Down
67 changes: 47 additions & 20 deletions v2/goopt.go
Original file line number Diff line number Diff line change
Expand Up @@ -1779,13 +1779,7 @@ func (p *Parser) PrintUsageWithGroups(writer io.Writer, config ...*PrettyPrintCo
if len(config) > 0 {
prettyPrintConfig = config[0]
} else {
prettyPrintConfig = &PrettyPrintConfig{
NewCommandPrefix: " + ",
DefaultPrefix: " ├─ ",
TerminalPrefix: " └─ ",
InnerLevelBindPrefix: " ** ",
OuterLevelBindPrefix: " │ ",
}
prettyPrintConfig = p.DefaultPrettyPrintConfig()
}

p.PrintPositionalArgs(writer)
Expand Down Expand Up @@ -1929,21 +1923,22 @@ func (p *Parser) PrintCommandsWithFlags(writer io.Writer, config *PrettyPrintCon
for kv := p.registeredCommands.Front(); kv != nil; kv = kv.Next() {
if kv.Value.topLevel {
kv.Value.Visit(func(cmd *Command, level int) bool {
// Determine the correct prefix based on command level and position
var prefix string
// Determine the tree prefix based on command level and position
var treePrefix string
switch {
case level == 0:
prefix = config.NewCommandPrefix
treePrefix = ""
case len(cmd.Subcommands) == 0:
prefix = config.TerminalPrefix
treePrefix = config.TerminalPrefix
default:
prefix = config.DefaultPrefix
treePrefix = config.DefaultPrefix
}

// Print the command itself with proper indentation
// Use CommandUsage to get the properly formatted command with positionals
commandStr := p.renderer.CommandUsage(cmd)
command := fmt.Sprintf("%s%s%s\n", prefix, strings.Repeat(config.InnerLevelBindPrefix, level), commandStr)
// All commands get the + marker (NewCommandPrefix)
command := fmt.Sprintf("%s%s%s\n", treePrefix, config.NewCommandPrefix, commandStr)
if _, err := writer.Write([]byte(command)); err != nil {
return false
}
Expand All @@ -1959,14 +1954,28 @@ func (p *Parser) PrintCommandsWithFlags(writer io.Writer, config *PrettyPrintCon

// PrintCommandSpecificFlags print flags for a specific command with the appropriate indentation
func (p *Parser) PrintCommandSpecificFlags(writer io.Writer, commandPath string, level int, config *PrettyPrintConfig) {
// Build indentation: outer line(s) + inner line
// For level 0: " │ │ " (one outer + inner separator)
// For level 1: " │ │ │ " (two outer + inner separator)
indent := strings.Repeat(config.OuterLevelBindPrefix, level+1) + config.InnerLevelBindPrefix

// First, collect and display positional arguments
positionals := p.getPositionalsForCommand(commandPath)
for _, pos := range positionals {
positionalUsage := p.renderer.PositionalUsage(pos.Argument, pos.Position)
output := fmt.Sprintf("%s%s\n", indent, positionalUsage)
_, _ = writer.Write([]byte(output))
}

// Then display regular flags
for f := p.acceptedFlags.Front(); f != nil; f = f.Next() {
if f.Value.CommandPath == commandPath {
// Skip positional arguments as they're shown inline with the command
// Skip positional arguments - already displayed above
if f.Value.Argument.isPositional() {
continue
}

flag := fmt.Sprintf("%s%s\n", strings.Repeat(config.OuterLevelBindPrefix, level+1), p.renderer.FlagUsage(f.Value.Argument))
flag := fmt.Sprintf("%s%s\n", indent, p.renderer.FlagUsage(f.Value.Argument))

_, _ = writer.Write([]byte(flag))
}
Expand All @@ -1979,6 +1988,11 @@ func (p *Parser) PrintFlags(writer io.Writer) {
printedFlags := make(map[string]bool)

for f := p.acceptedFlags.Front(); f != nil; f = f.Next() {
// Skip positional arguments - they are shown inline with commands
if f.Value.Argument.isPositional() {
continue
}

// Extract the base flag name (without command path)
flagKey := *f.Key
flagParts := splitPathFlag(flagKey)
Expand All @@ -1989,11 +2003,6 @@ func (p *Parser) PrintFlags(writer io.Writer) {
continue
}

// Skip positional arguments
if f.Value.Argument.isPositional() {
continue
}

// Mark as printed and output
printedFlags[baseFlagName] = true
_, _ = writer.Write([]byte(fmt.Sprintf(" %s\n", p.renderer.FlagUsage(f.Value.Argument))))
Expand All @@ -2015,6 +2024,16 @@ func (p *Parser) GetHelpConfig() HelpConfig {
return p.helpConfig
}

// SetPrettyPrintConfig sets the configuration for pretty-printing command trees and help output
func (p *Parser) SetPrettyPrintConfig(config *PrettyPrintConfig) {
p.prettyPrintConfig = config
}

// GetPrettyPrintConfig returns the current pretty-print configuration, or nil if not set
func (p *Parser) GetPrettyPrintConfig() *PrettyPrintConfig {
return p.prettyPrintConfig
}

// PrintHelp prints help according to the configured style
func (p *Parser) PrintHelp(writer io.Writer) {
style := p.helpConfig.Style
Expand All @@ -2029,6 +2048,8 @@ func (p *Parser) PrintHelp(writer io.Writer) {
p.printFlatHelp(writer)
case HelpStyleGrouped:
p.printGroupedHelp(writer)
case HelpStyleGroupedClean:
p.printGroupedCleanHelp(writer)
case HelpStyleCompact:
p.printCompactHelp(writer)
case HelpStyleHierarchical:
Expand All @@ -2041,6 +2062,12 @@ func (p *Parser) PrintHelp(writer io.Writer) {
}

func (p *Parser) DefaultPrettyPrintConfig() *PrettyPrintConfig {
// Return custom config if set
if p.prettyPrintConfig != nil {
return p.prettyPrintConfig
}

// Otherwise return default config
return &PrettyPrintConfig{
NewCommandPrefix: " + ",
DefaultPrefix: " ├─ ",
Expand Down
36 changes: 29 additions & 7 deletions v2/goopt_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2974,7 +2974,12 @@ func (p *Parser) detectBestStyle() HelpStyle {
return HelpStyleGrouped
}

// Simple CLI
// CLI with any commands uses grouped style for clarity
if cmdCount > 0 {
return HelpStyleGrouped
}

// Simple CLI with no commands - flat style
return HelpStyleFlat
}

Expand All @@ -2988,6 +2993,18 @@ func (p *Parser) printGroupedHelp(writer io.Writer) {
p.PrintUsageWithGroups(writer)
}

// printGroupedCleanHelp prints grouped help with clean, compact formatting (no ** markers, tighter spacing)
func (p *Parser) printGroupedCleanHelp(writer io.Writer) {
cleanConfig := &PrettyPrintConfig{
NewCommandPrefix: " + ",
DefaultPrefix: " ├─ ",
TerminalPrefix: " └─ ",
InnerLevelBindPrefix: " ", // 2 spaces - clean and compact
OuterLevelBindPrefix: " │ ",
}
p.PrintUsageWithGroups(writer, cleanConfig)
}

// printCompactHelp prints deduplicated, compact help
func (p *Parser) printCompactHelp(writer io.Writer) {
fmt.Fprintln(writer, p.layeredProvider.GetFormattedMessage(messages.MsgUsageKey, os.Args[0]))
Expand Down Expand Up @@ -3063,9 +3080,14 @@ func (p *Parser) printCompactHelp(writer io.Writer) {
}
}

fmt.Fprintf(writer, " %-15s %-40s",
cmdName,
util.Truncate(desc, 40))
if desc != "" {
fmt.Fprintf(writer, " %-15s \"%-40s\"",
cmdName,
util.Truncate(desc, 40))
} else {
fmt.Fprintf(writer, " %-15s %-40s",
cmdName, "")
}
if flagCount > 0 {
fmt.Fprintf(writer, " [%d %s]", flagCount, p.layeredProvider.GetMessage(messages.MsgFlagsKey))
}
Expand Down Expand Up @@ -3277,7 +3299,7 @@ func (p *Parser) printCommandTree(writer io.Writer) {

desc := p.renderer.CommandDescription(cmd.Value)
if desc != "" {
fmt.Fprintf(writer, "\n%-20s %s\n", cmdName, desc)
fmt.Fprintf(writer, "\n%-20s \"%s\"\n", cmdName, desc)
} else {
fmt.Fprintf(writer, "\n%s\n", cmdName)
}
Expand Down Expand Up @@ -3307,10 +3329,10 @@ func (p *Parser) printCommandTree(writer io.Writer) {

if registeredSub, found := p.registeredCommands.Get(subPath); found {
desc := util.Truncate(p.renderer.CommandDescription(registeredSub), 50)
fmt.Fprintf(writer, " %s %-20s %s\n", prefix, subName, desc)
fmt.Fprintf(writer, " %s %-20s \"%s\"\n", prefix, subName, desc)
} else {
desc := util.Truncate(p.renderer.CommandDescription(sub), 50)
fmt.Fprintf(writer, " %s %-20s %s\n", prefix, subName, desc)
fmt.Fprintf(writer, " %s %-20s \"%s\"\n", prefix, subName, desc)
}
}
}
Expand Down
30 changes: 17 additions & 13 deletions v2/goopt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1394,12 +1394,12 @@ Global Flags:

Commands:
+ create "Create resources"
├─ ** create user "Manage users"
│ │ --email or -e "Email for user creation" (optional)
└─ ** ** create user type "Specify user type"
│ │ │ --username "Username for user creation" (required)
│ │ │ --firstName "User first name" (optional)
└─ ** create group "Manage groups"
├─ + create user "Manage users"
│ │ ** --email or -e "Email for user creation" (optional)
└─ + create user type "Specify user type"
│ │ │ ** --username "Username for user creation" (required)
│ │ │ ** --firstName "User first name" (optional)
└─ + create group "Manage groups"
`
output := strings.Join(*writer.data, "")
assert.Equal(t, expectedOutput, output, "usage output should be grouped and formatted correctly")
Expand Down Expand Up @@ -1467,11 +1467,11 @@ func TestParser_PrintUsageWithCustomGroups(t *testing.T) {
opts.PrintCommandsWithFlags(writer, printConfig)

expectedOutput := ` + create "Create resources"
* create user "Manage users"
└ └ --email "Email for user creation" (optional)
* * create user type "Specify user type"
└ └ └ --username "Username for user creation" (required)
* create group "Manage groups"
+ create user "Manage users"
└ └ * --email "Email for user creation" (optional)
+ create user type "Specify user type"
└ └ └ * --username "Username for user creation" (required)
+ create group "Manage groups"
`

// Check that the printed output matches the expected structure
Expand Down Expand Up @@ -10244,6 +10244,8 @@ func TestParser_SetRenderer(t *testing.T) {
})
assert.NoError(t, err)

// Explicitly set flat style for this test since we're testing custom renderer format
p.SetHelpStyle(HelpStyleFlat)
p.PrintHelp(output)
// ensure table-like output from CustomRenderer
assert.Contains(t, output.String(), ` -f, --flag1 Flag 1
Expand Down Expand Up @@ -13938,7 +13940,8 @@ func TestParser_CommandHierarchyDescriptions(t *testing.T) {
// Note: Without translations set up, descriptionKeys are shown instead of descriptions.
// This is intentional - it helps catch regressions where descriptionKeys might be
// incorrectly propagated from child to parent commands.
if !strings.Contains(output, "middle middle.desc") {
// Descriptions now have quotes
if !strings.Contains(output, "middle \"middle.desc\"") {
t.Errorf("Hierarchical help doesn't show correct description key for middle command.\nOutput:\n%s", output)
}
}
Expand Down Expand Up @@ -14027,7 +14030,8 @@ func TestParser_HierarchicalHelpRegression(t *testing.T) {
output := buf.String()

// The bug was that "copy" showed "copy blob configuration" instead of "nexus copy commands"
if !strings.Contains(output, "copy nexus copy commands") {
// Descriptions now have quotes
if !strings.Contains(output, "copy \"nexus copy commands\"") {
t.Errorf("Hierarchical help shows wrong description for copy command.\nOutput:\n%s", output)
}
}
Expand Down
Loading
Loading