zio / zio/zio-cli

--help on a non-first subcommand shows the first subcommand's help instead

Open Beginner friendly
#586 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
138
Forks
91
Avg merge
1d 8h
Merged PRs (30d)
13

Description

Summary

root <subcommand> --help renders the first registered subcommand's help instead of the one the user named. This affects every non-first subcommand, and nested cases (root parent child --help). Root-level root --help is correct.

Related to #448 / #576: those fixed the bare --help case (no subcommand named, which dived into the first subcommand) and the cmd --help payload leftover case. They did not cover an explicitly named non-first subcommand, so this is a distinct, still-open facet. Note that #576 is already included in the 0.8.1 release, so upgrading does not fix this.

Reproduction
val tool = Command("tool").subcommands(
  Command("start", Options.text("port")),
  Command("stop")
)

tool.parse(List("tool", "stop", "--help"), CliConfig.default)
  • Expected: help for stop.
  • Actual: help for start (e.g. tool start [--port <text>]).

Reproduced on current master, which is identical to the 0.8.1 release in this code path.

Root cause (Command.scala)
  1. Command.Single.parse — the final-check exhaustiveSearch forces this command's own name:

    if (args.contains("--help") || args.contains("-h")) parseBuiltInArgs(List(name, "--help"))
    

    So a Single returns its own help whenever --help appears anywhere in args, even when args do not name this command.

  2. Command.Subcommands.parse — the UserDefined branch parses the child with the original conf (finalCheckBuiltIn = true):

    child.parse(leftover, conf)
    

    instead of the childConf (finalCheckBuiltIn = false) it already defines and uses in the dedicated help/wizard paths.

Trace for tool stop --help:

  • Root Single("tool") parses, leaving ["stop", "--help"] → the UserDefined branch.
  • The child is a left-leaning OrElse(start, stop), so start.parse(["stop", "--help"], conf) runs first.
  • start's name does not match stop, so normal parsing fails — but finalCheckBuiltIn == true and args.contains("--help"), so exhaustiveSearch fires and returns start's help.
  • OrElse sees a success and short-circuits; stop is never tried.
Suggested fix

Reuse childConf in the UserDefined branch (consistent with the help/wizard paths in the same method):

case CommandDirective.UserDefined(leftover, a) if leftover.nonEmpty =>
  child.parse(leftover, childConf)   // was: conf

With finalCheckBuiltIn = false for the child OrElse, a non-matching alternative fails with CommandMismatch instead of returning its own help, so OrElse advances to the alternative whose name matches. Optionally also guard exhaustiveSearch so it only fires when the leading token in args names this command.

Test gap

The current subcommand-help test only checks git add --help, where add is the first subcommand — which hides this bug. A regression test should name a non-first subcommand, e.g. git clone --help must show clone's help and not the first subcommand's.

Workaround
config = CliConfig.default.copy(finalCheckBuiltIn = false)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in Command.scala, especially Command.Single.parse and Command.Subcommands.parse, then read the existing subcommand-help test. Reproduce the non-first case such as git clone --help, apply the suggested child parsing configuration consistently with the help paths, and add a regression test showing that the named subcommand's help is rendered rather than the first subcommand's.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
cli
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.