Introduce subcommand grouping into the command configuration to improve help#644
Conversation
…ve help
Add optional support for grouping subcommands into named groups, to
help bring order to commands with many subcommands without requiring
additional structure. For example, here's the help for a
"subgroupings" command that has an ungrouped subcommand (m), and two
groups of subcommands ("broken" and "complicated").
USAGE: subgroupings <subcommand>
OPTIONS:
-h, --help Show help information.
SUBCOMMANDS:
m
BROKEN SUBCOMMANDS:
foo Perform some foo
bar Perform bar operations
COMPLICATED SUBCOMMANDS:
n
See 'subgroupings help <subcommand>' for detailed help.
To be able freely mix subcommands and subcommand groups, CommandConfiguration
has a new initializer that takes a result builder. The help output
above is created like this:
struct WithSubgroups: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "subgroupings"
) {
CommandGroup(name: "Broken") {
Foo.self
Bar.self
}
M.self
CommandGroup(name: "Complicated") {
N.self
}
}
}
Each `CommandGroup` names a new group and is given commands (there are
no groups within groups). The other entries are arbitrary
ParsableCommands.
This structure is only cosmetic, and only affects help generation by
providing more structure for the reader. It doesn't impact existing
clients, who can still reason about the flattened list of subcommands
if they prefer.
|
@swift-ci please test |
|
I was just looking at doing something very similar! (WIP here: https://github.com/apple/swift-argument-parser/tree/subcommand-groups). The CommandsBuilder idea is an interesting one. However is it limiting in some of the manipulation that you can do with the existing simple array |
|
Do we think there's value in allowing for a longer description string for the group? For example how subcommand groups are described in |
It shouldn't be too limiting. Result builders can support more language syntax if we'd like (e.g., the ability to do
I'd expect
Yes, I think that's a good idea! It's easy enough to add an "abstract" that we put here. |
…syntax Adds support for if, if-else, if #available, and for..in loops.
|
@swift-ci please test |
|
@dickoff See the latest two commits, which add an optional abstract and then extend out the result builders to handle all of the declarative syntax. |
|
I've opened up a discussion thread on the forums for this change: https://forums.swift.org/t/grouping-subcommands/72219 |
New changes look great! They resolve all the questions I brought up. |
|
I would recommend splitting out the abstract into a different PR. If we add this functionality to CommandGroups then we should probably add them OptionGroups and that feels like scope creep for this change. |
natecook1000
left a comment
There was a problem hiding this comment.
Thanks so much for this, @DougGregor! In addition to the notes below, could you add initializers that just accept arrays that are parallel to the builder-based initializers for CommandConfiguration and CommandGroup? That will be more flexible for projects that set up the command hierarchy out of place with the command declarations themselves.
| var renderedHeader = String(describing: header).uppercased() + ":" | ||
| if let abstract = header.abstract { | ||
| renderedHeader += " " | ||
| renderedHeader += abstract.wrapped(to: screenWidth) |
There was a problem hiding this comment.
Do we need to add the abstract and then wrap?
There was a problem hiding this comment.
Ahhh, probably. Thank you
| // - grouped subcommands | ||
| // - ungrouped subcommands |
There was a problem hiding this comment.
Looks like this order is flipped from the actual behavior.
| } | ||
|
|
||
| /// Describes a single subcommand or a group thereof. | ||
| public enum Subcommand: Sendable { |
There was a problem hiding this comment.
This name doesn't quite describe what the type represents, since a group of subcommands is not itself a subcommand. One option is CommandGroup – I'm more comfortable with a CommandGroup that just stores a single command than a Subcommand that stores a bunch of commands. But then we couldn't use that name for the other type, which it's perfect for.
Maybe just making it Subcommands is an improvement? I don't think this is a dealbreaker – do you have any other ideas for this name?
| /// are `-h` and `--help`. | ||
| /// - aliases: An array of aliases for the command's name. All of the aliases | ||
| /// MUST not match the actual command name, whether that be the derived name | ||
| /// if `commandName` is not provided, or `commandName` itself if provided. |
There was a problem hiding this comment.
Something like this:
| /// if `commandName` is not provided, or `commandName` itself if provided. | |
| /// if `commandName` is not provided, or `commandName` itself if provided. | |
| /// - groupedSubcommands: A builder closure that defines the subcommands | |
| /// and their groups for this command. |
Hmm. If we're going to do this, I think I'd rather drop the builder-based initializers entirely from this pull request than introduce two new interfaces for the same thing. It's completely reasonable to say that the only way to get this functionality is with syntax like this: subcommands: [
// all of the ungrouped subcommands
],
groupedSubcommands: [
CommandGroup(
name: "group name",
subcommands: [
// subcommands in this group
]
),
// ...
].That avoids having to create the |
|
That makes sense! I could see that fitting into a whole builder-based configuration approach (which I think we've looked at before). If we make this change, let's also make |
Could we not just have the existing |
I think we need |
This reverts commit ab563a2.
…rray Introduce subcommand groups with a more modest extension to the API that adds another array of subcommand groups alongside the (ungrouped) subcommands array. We can consider introducing result builders as a separate step later, if there's more to be gained from it.
|
@swift-ci please test |
|
@natecook1000 I believe I addressed all of your comments. The updated PR description contains the non-result-builder interface, which is a more straightforward extension of the existing syntax: struct WithSubgroups: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "subgroupings",
subcommands: [ M.self ],
groupedSubcommands: [
CommandGroup(
name: "Broken",
subcommands: [ Foo.self, Bar.self ]
),
CommandGroup(name: "Complicated", subcommands: [ N.self ]),
CommandGroup(name: "Invisible", subcommands: [ ])
]
)
} |
rauhul
left a comment
There was a problem hiding this comment.
Looks like a great addition and UX improvement.
| /// An array of the types that define subcommands for this command. | ||
| public var subcommands: [ParsableCommand.Type] | ||
|
|
||
| /// |
There was a problem hiding this comment.
Can we drop this stray doc comment line?
There was a problem hiding this comment.
I was using the extra line to separate the "abstract" line from the "detail" line.
There was a problem hiding this comment.
Wow I can't this morning, I made like 2 other mistakes similar, sorry about the noise
natecook1000
left a comment
There was a problem hiding this comment.
Looks great, thanks again!
| /// An array of types that define subcommands for this command and are | ||
| /// not part of any command group. | ||
| public var ungroupedSubcommands: [ParsableCommand.Type] | ||
|
|
||
| /// The list of subcommands and subcommand groups. | ||
| public var groupedSubcommands: [CommandGroup] |
Add optional support for grouping subcommands into named groups, to help bring order to commands with many subcommands without requiring additional structure. For example, here's the help for a "subgroupings" command that has an ungrouped subcommand (m), and two groups of subcommands ("broken" and "complicated").
The help output above is created like this:
Each
CommandGroupnames a new group with its subcommands (there are no groups within groups).This structure is only cosmetic, and only affects help generation by providing more structure for the reader. It doesn't impact existing clients, who can still reason about the flattened list of subcommands if they prefer.