Skip to content

Allow subcommands to reject inherited options for a subtree #2518

Description

@garretwilson

This is a follow-on to #649. I don't expect that you will add this addition any time soon, and that's fine—I'm finishing up a workaround right now (described at the end). I'm just filing this feature request for completeness and for visibility. The description below is LLM-generated.

Use Case

Picocli's ScopeType.INHERIT is useful for options declared on a top-level command that should normally be accepted by all descendants. Sometimes, however, a subtree should not accept one of those inherited options at all.

Example:

@Command(name = "dbctl", subcommands = {BackupCommand.class, ServerCommand.class})
class DbCtl implements Runnable {

    @Option(names = "--database", scope = ScopeType.INHERIT)
    String database;

    // ...
}

@Command(name = "server", subcommands = {StartCommand.class, StopCommand.class})
class ServerCommand implements Runnable {
    // `--database` is inherited syntactically, but it is not meaningful for
    // `server` commands because they manage the server process, not a database.
}

Because --database is inherited, all of these are syntactically valid today:

dbctl --database main server start
dbctl server --database main start
dbctl server start --database main

For this use case, the desired behavior is for the entire server subtree to reject --database, regardless of which command level the user typed the inherited option at.

Hypothetical API

A command-level annotation attribute seems like a natural fit, because the semantic rule belongs to the subtree where the inherited option is not applicable:

@Command(
    name = "server",
    rejectInheritedOptions = "--database",
    subcommands = {StartCommand.class, StopCommand.class}
)
class ServerCommand implements Runnable {
    // ...
}

Equivalent programmatic API might look like:

serverSpec.rejectInheritedOptions("--database");

The exact API names are only illustrative. Other names such as forbiddenInheritedOptions, excludedInheritedOptions, or a richer annotation would also make sense.

Expected Semantics

If a command rejects inherited option --database, that rejection applies to the command and its descendants.

For the example above, all of these invocations would fail with a ParameterException:

dbctl --database main server start
dbctl server --database main start
dbctl server start --database main

The rejection should be based on the canonical inherited option, not merely string comparison, so aliases and inherited copies are handled consistently.

Possible Implementation Sketch

Picocli already copies ScopeType.INHERIT options into descendant CommandSpecs as inherited OptionSpecs. These inherited copies retain canonical identity via ArgSpec.root().

A native implementation could likely be modeled as command-level metadata on CommandSpec, storing the inherited option names or canonical option roots rejected by that command.

One important detail is that validation probably needs to happen after the full command line has been parsed, not only when an option token is first matched. For example:

dbctl --database main server start

When --database is matched, picocli is still parsing the root dbctl command and has not yet reached the later server start subtree. A post-parse validation pass can see both facts:

  1. the invoked command path contains server;
  2. the inherited --database option was explicitly matched somewhere in the invocation.

At a high level, such a validation pass could:

  1. Walk the ParseResult tree or active command path.
  2. Collect matched options from each parse level.
  3. For each command in the invoked path with rejected inherited options:
    • resolve the rejected option name against that command's CommandSpec;
    • verify that the resolved option is inherited;
    • canonicalize via ArgSpec.root();
    • compare against the canonical root of each matched option.
  4. Throw ParameterException if a rejected inherited option was matched anywhere in the invocation.

There are several design details for maintainers to decide: alias handling, negatable options, repeatable subcommands, usage help display, localization of the error message, mixins, method commands, and the exact programmatic API.

Current Workaround

In globalmentor-application we are planning a workaround in BaseCliApplication / BaseCliSubcommand. The workaround has not been committed yet, but the planned approach is to provide runtime validation helpers such as:

rejectIfMatched("--database", "Option `--database` is not applicable to `server` commands.");

These helpers walk the actively invoked command path after parsing and compare matched options by canonical ArgSpec.root() identity, so they can detect inherited options regardless of whether the user typed the option at the root, parent, or leaf command level.

This workaround is useful for application-level validation, including value-dependent checks such as rejecting --color when a positional parameter has a certain value. However, for the inherited-option subtree case, a native picocli feature would be better because it could be declarative, parser-enforced, and subtree-wide without requiring every executable leaf command to remember to call a validation helper.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions