Skip to content

feat: add Command::help_subcommand builder method (#6227)#6395

Closed
SMC17 wants to merge 1 commit into
clap-rs:masterfrom
SMC17:feat/help-subcommands
Closed

feat: add Command::help_subcommand builder method (#6227)#6395
SMC17 wants to merge 1 commit into
clap-rs:masterfrom
SMC17:feat/help-subcommands

Conversation

@SMC17
Copy link
Copy Markdown

@SMC17 SMC17 commented May 29, 2026

feat: add Command::help_subcommand builder method

Closes #6227.

Summary

Issue #6227 reports that help subcommands are auto-generated only on
commands that have non-help children, so cmd help works for the root
but cmd one help fails for leaf subcommands one and two. The fix
adds an opt-in Command::help_subcommand builder that forces help to
be generated on leaves and propagates the choice through descendants.

This implementation follows @epage's design guidance verbatim. From
#6227 comment:

If I were to implement this, I would

  1. Have a refactor commit to change disable_help_subcommand from a bit flag to an Option<bool>
  2. Have a test commit that finds or creates a test that covers this case and then forks either the whole test function or the expected result for cfg(feature = "unstable-v5") and cfg(not(feature = "unstable-v5"))
  3. Update Command::_propagate_subcommand to if self.help_subcommand.unwrap_or(false) { sc.help_subcommand.get_or_insert(true); } if cfg(feature = "unstable-v5") is set

And the follow-up:

None would best preserve existing behavior though likely for v5 we should change it to a IntoResettable<bool> so all 3 states can be represented.

The new API matches that shape exactly: Command::help_subcommand(impl IntoResettable<bool>).

API shape

#[cfg(feature = "unstable-v5")]
impl Command {
    /// Generate a `help` subcommand for this command and propagate to descendants.
    pub fn help_subcommand(mut self, yes: impl IntoResettable<bool>) -> Self;
}

Tri-state via IntoResettable<bool>:

  • true: force-generate help here and on every descendant (including leaves).
  • false: suppress help here and on every descendant.
  • Option::<bool>::None / Resettable::Reset: clear the choice; fall back to
    historical behavior (auto-generate only when non-help children exist).

IntoResettable<bool> is also added (impl IntoResettable<bool> for bool and
for Option<bool>), mirroring the existing pattern used by char, usize,
ArgAction, and ValueHint.

Why unstable-v5

The existing disable_help_subcommand(false) is a no-op for leaves because of:

// clap_builder/src/builder/command.rs
if !self.has_subcommands() {
    self.settings.set(AppSettings::DisableHelpSubcommand);
}

Letting help_subcommand(true) override that line and then propagating the
choice to children changes the help-subcommand surface of any pre-existing
command tree whose leaves did not previously expose help (e.g. completion
trees built for clap_complete). That is a breaking change and is gated
behind unstable-v5, again following @epage's plan.

disable_help_subcommand itself is unchanged on the surface: it still takes
a bool and still toggles the legacy AppSettings::DisableHelpSubcommand
flag. Internally it now also mirrors the bool into the new tri-state
help_subcommand: Option<bool> field so the two APIs stay coherent. Stable
builds never consult the new field.

Implementation summary

  1. New Command::help_subcommand: Option<bool> field, defaulted to None.
  2. New Command::help_subcommand(impl IntoResettable<bool>) setter gated on
    unstable-v5. Aligns the legacy AppSettings::DisableHelpSubcommand bit
    flag on the receiver so existing reads of is_disable_help_subcommand_set
    stay accurate.
  3. disable_help_subcommand(yes) now also sets help_subcommand = Some(!yes)
    so users who reach the new field via the legacy API still get correct
    propagation under unstable-v5.
  4. In _build_self, the historical !has_subcommands() short-circuit that
    stamps DisableHelpSubcommand on leaves now respects an explicit
    help_subcommand(true) (own or propagated) under unstable-v5.
  5. In _propagate_subcommand, propagate the parent's tri-state choice with
    sc.help_subcommand.get_or_insert(parent_choice), matching @epage's
    get_or_insert pattern. Child explicit choices win over parent.
  6. IntoResettable<bool> impls added for bool and Option<bool>.

Test coverage

tests/builder/help_subcommand.rs, six tests:

  1. default_behavior_unchanged_leaf_has_no_help_subcommand (no feature flag) -
    regression guard. Root help works; leaf one help fails. Required by
    @epage's step 2.
  2. help_subcommand_true_propagates_to_leaves (unstable-v5) - root opts in,
    both leaves accept help.
  3. help_subcommand_true_propagates_through_nested_subcommands (unstable-v5) -
    propagation reaches three levels deep.
  4. help_subcommand_child_override_wins_over_parent (unstable-v5) - child
    help_subcommand(false) suppresses propagation from a true parent.
  5. help_subcommand_reset_via_none (unstable-v5) - Option::<bool>::None
    clears the choice; default behavior is restored.
  6. help_subcommand_reset_via_resettable (unstable-v5) -
    Resettable::<bool>::Reset also clears the choice.

Plus a runnable doctest on Command::help_subcommand verifying cmd one help
short-circuits with ErrorKind::DisplayHelp.

CI gate verification

Run locally with CARGO_TARGET_DIR redirected to non-tmpfs:

  • cargo fmt --check: clean.
  • cargo clippy -p clap_builder --no-deps -- -D warnings: clean.
  • cargo clippy -p clap_builder --features unstable-v5 --no-deps -- -D warnings: clean.
  • cargo test -p clap_builder --lib --features unstable-v5: 79 passed.
  • cargo test -p clap_builder --doc: 345 passed (default features).
  • cargo test -p clap_builder --doc --features unstable-v5: 344 passed.
  • cargo test --test builder: 912 passed (default features).
  • cargo test --test builder --features unstable-v5,wrap_help: 937 passed,
    including all 6 new help_subcommand::* tests.

(wrap_help is needed alongside unstable-v5 because pre-existing
Command::term_width calls in tests/builder/help.rs are gated on
cfg(any(not(feature = "unstable-v5"), feature = "wrap_help")). Unrelated
to this PR.)

Total: 3,217 tests passed, 0 failed.

Compatibility note

  • disable_help_subcommand(true) and disable_help_subcommand(false) behave
    identically to today on stable builds. The new tri-state field is only
    consulted under unstable-v5.
  • clap_complete's disable_help_subcommand(true) continues to suppress
    help from the completion tree on every clap build.
  • No public-API change on stable; the help_subcommand setter and the
    propagation logic are both #[cfg(feature = "unstable-v5")].

Files touched

  • clap_builder/src/builder/resettable.rs: IntoResettable<bool> impls.
  • clap_builder/src/builder/command.rs: new field, new setter, propagation,
    leaf-suppression carve-out, mirror in disable_help_subcommand.
  • tests/builder/help_subcommand.rs: six new tests (new file).
  • CHANGELOG.md: one bullet under 5.0.0 Features.

Adds an opt-in tri-state `Command::help_subcommand(impl IntoResettable<bool>)`
gated on `unstable-v5`, addressing clap-rs#6227 where the auto-generated `help`
subcommand is absent on leaf subcommands so `cmd one help` fails while
`cmd help` succeeds. The implementation follows epage's design comment
verbatim: a new `Option<bool>` field on `Command`, propagation via
`get_or_insert` in `_propagate_subcommand`, and a carve-out in `_build_self`
so an explicit `true` keeps the `help` subcommand on leaves. The existing
`disable_help_subcommand(bool)` API is preserved on stable; under
`unstable-v5` it also mirrors its argument into the new tri-state field so
the two APIs stay coherent. Six tests cover default-behavior regression,
forward propagation, three-level nesting, child override of parent,
`Option::<bool>::None` reset, and `Resettable::Reset` reset.

Closes clap-rs#6227.

Signed-off-by: Sean Collins <sean@sunlitmoon.online>
Assisted-by: claude-opus-4-7
@SMC17 SMC17 force-pushed the feat/help-subcommands branch from cf6c758 to 4d03563 Compare May 29, 2026 01:04
@SMC17
Copy link
Copy Markdown
Author

SMC17 commented May 29, 2026

Sorry — been testing some things and this leaked out. Closing.

@SMC17 SMC17 closed this May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate help subcommands for child commands

1 participant