Skip to content

Conversation

@piotrooo
Copy link
Contributor

@piotrooo piotrooo commented Nov 16, 2025

Context

Important

Before we can revisit the available options for commands, we need to have a robust solution for command registration and lookup — even when commands are nested.

Let's imagine the following input:

shell:> cmd sub-cmd1 sub-cmd2 arg1 arg2

So where is the full command path? How can it be distinguished from positional arguments (arg1, arg2)?
It’s crucial to have a reliable mechanism for registering and looking up commands.

Nested commands are also a very useful feature, so we need to support them as well.

What changed

  • Introduced the CommandTree.Node structure for registering commands, which enables proper command lookup. The tree structure organizes commands in a parent–child relationship, with the executable command located at the leaf.
(root)
  ├── (one)    -> command "root one"
  └── (child)  -> command "root child"
        ├── (first)  -> command "root child first"
        └── (second) -> command "root child second"
  • Added the ElementType.TYPE target to the @Command annotation to support sub-commands.
  • Removed the ability to define multiple command names in the @Command annotation, as a command name should naturally be a single word (although a name such as "cmd other-word" is still possible).
  • When the command name (@Command(name = "cmd")) is not specified, the method name is used as a fallback.
  • Refactored CommandOption as record.
  • Reverted the .editorconfig file to help developers with differently configured IDEs (spaces vs. tabs).

Signed-off-by: Piotr Olaszewski <[email protected]>
@fmbenhassine
Copy link
Contributor

Thank you for the PR! Sub command support is planned for 4.0 M3. I will review this after we release 4.0 M2 (including the new DSL #1211)

@fmbenhassine fmbenhassine added this to the 4.0.0-M3 milestone Nov 27, 2025
@fmbenhassine
Copy link
Contributor

I was looking into this and got a couple thoughts that I wanted to share here.

shell:> cmd sub-cmd1 sub-cmd2 arg1 arg2
So where is the full command path? How can it be distinguished from positional arguments (arg1, arg2)?

This is one of the reasons why named flags (options) are better than positional arguments, see CLI Guidelines and 12 Factor CLI Apps

The POSIX style has the "end of options" -- notation to separate sub commands / options from arguments, see "Guideline 10" here, which is supported in Picocli. So in the example you gave, the user would invoke the subcommand with:

shell:> cmd sub-cmd1 sub-cmd2 -- arg1 arg2

Some other folks use ":" to separate arguments from subcommands, see "11. Be clear about subcommands" in https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46.

Another approach I have seen in other (non java) CLI frameworks is to only support flags (ie options) when using sub commands, see "Any struct that contains a subcommand must not contain any positionals" in this popular Go CLI parser for example.


That said, I think we can support sub-commands by using one these two approaches (ie either ask users to use a separator or not to use arguments with subcommands), rather than introducing any complexity in the framework to support ambiguous syntax like "cmd sub-cmd1 sub-cmd2 arg1 arg2" with command trees and parent/child relationships.

Please don't get me wrong, the approach in this PR is interesting! I am just trying to ask the right questions (to see if we should tackle this problem at all) in order to keep things as simple as possible in the framework. Thoughts?

@fmbenhassine fmbenhassine added the status/need-feedback Calling participant to provide feedback label Nov 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status/need-feedback Calling participant to provide feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants