-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Description
My urfave/cli version is v3.6.2
Checklist
- Are you running the latest v3 release? The list of releases is here.
- Did you check the manual for your release? The v3 manual is here
- Did you perform a search about this problem? Here's the GitHub guide about searching.
Dependency Management
- My project is using go modules.
Describe the bug
Setting HideHelpCommand: true on a parent command causes subcommand help to use SubcommandHelpTemplate instead of CommandHelpTemplate, which results in GLOBAL OPTIONS (persistent flags from the root command) not being displayed in subcommand help output.
The root cause is in helpCommandAction() in help.go. The template selection logic at lines 125-140 depends on len(cmd.Commands):
if (len(cmd.Commands) == 1 && !cmd.HideHelp) || (len(cmd.Commands) == 0 && cmd.HideHelp) {
tmpl = CommandHelpTemplate // Has GLOBAL OPTIONS
} else {
ShowSubcommandHelp(cmd) // Uses SubcommandHelpTemplate - NO GLOBAL OPTIONS
}When HideHelpCommand: true is set:
ensureHelp()does NOT add the internalhelpsubcommand tocmd.Commands- Subcommand ends up with
len(Commands) == 0 - With
HideHelp = false(default), condition evaluates tofalse - Falls through to
ShowSubcommandHelp()→ usesSubcommandHelpTemplate→ GLOBAL OPTIONS missing
When HideHelpCommand: false (default):
ensureHelp()adds the internalhelpsubcommand- Subcommand ends up with
len(Commands) == 1 - Condition evaluates to
true - Uses
CommandHelpTemplate→ GLOBAL OPTIONS displayed correctly
To reproduce
package main
import (
"context"
"fmt"
"os"
"github.com/urfave/cli/v3"
)
func main() {
app := &cli.Command{
Name: "myapp",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Path to config file",
},
},
HideHelpCommand: true, // This breaks GLOBAL OPTIONS in subcommand help
Commands: []*cli.Command{
{
Name: "serve",
Usage: "Start the server",
Action: func(ctx context.Context, c *cli.Command) error {
fmt.Println("serving...")
return nil
},
},
},
}
if err := app.Run(context.Background(), os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}Run:
go run main.go serve --helpObserved behavior
NAME:
myapp serve - Start the server
USAGE:
myapp serve
OPTIONS:
--help, -h show help
Note: GLOBAL OPTIONS section is missing - the --config flag is not shown.
Expected behavior
NAME:
myapp serve - Start the server
USAGE:
myapp serve [options]
OPTIONS:
--help, -h show help
GLOBAL OPTIONS:
--config string Path to config file
The GLOBAL OPTIONS section should be displayed regardless of whether HideHelpCommand is set, because:
HideHelpCommandis documented to hide thehelpsubcommand, not affect help output formatCommandHelpTemplateincludes{{if .VisiblePersistentFlags}}GLOBAL OPTIONS:...{{end}}SubcommandHelpTemplatelacks this section entirely- The choice between these templates should not depend on whether an internal help subcommand exists
Additional context
Workaround
Override cli.ShowSubcommandHelp to use CustomHelpTemplate when available:
cli.ShowSubcommandHelp = func(cmd *cli.Command) error {
tmpl := cmd.CustomHelpTemplate
if tmpl == "" {
tmpl = cli.SubcommandHelpTemplate
}
cli.HelpPrinter(cmd.Root().Writer, tmpl, cmd)
return nil
}And set CustomHelpTemplate on commands to include the GLOBAL OPTIONS section.
Suggested fix
Either:
- Add
{{if .VisiblePersistentFlags}}GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}}toSubcommandHelpTemplate - Or change the template selection logic in
helpCommandAction()to not depend onlen(cmd.Commands)which is affected byHideHelpCommand - Or make
DefaultShowSubcommandHelprespectCustomHelpTemplatelikeDefaultShowCommandHelpdoes
Want to fix this yourself?
Yes, I'd be happy to submit a PR. The simplest fix would be to add the GLOBAL OPTIONS section to SubcommandHelpTemplate in template.go:
var SubcommandHelpTemplate = `NAME:
{{template "helpNameTemplate" .}}
USAGE:
...existing template content...
OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}}
GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}}
`Run go version and paste its output here
go version go1.24.4 darwin/arm64
Run go env and paste its output here
AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE='on'
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/user/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/user/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/x2/3fr4kvl139zc880zh91n0j_c0000gp/T/go-build3762887804=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/user/Developer/P09580-SimulationToolkit/go.mod'
GOMODCACHE='/Users/user/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/user/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/user/Developer/.anyenv/envs/goenv/versions/1.24.4'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/user/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/user/Developer/.anyenv/envs/goenv/versions/1.24.4/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.4'
GOWORK=''
PKG_CONFIG='pkg-config'