-
Notifications
You must be signed in to change notification settings - Fork 340
Introduce subcommand grouping into the command configuration to improve help #644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to add the abstract and then wrap?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh, probably. Thank you
// - grouped subcommands | ||
// - ungrouped subcommands |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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: [ ])
]
)
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a great addition and UX improvement.
@@ -47,8 +47,27 @@ public struct CommandConfiguration: Sendable { | |||
public var shouldDisplay: Bool | |||
|
|||
/// An array of the types that define subcommands for this command. | |||
public var subcommands: [ParsableCommand.Type] | |||
|
|||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we drop this stray doc comment line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was using the extra line to separate the "abstract" line from the "detail" line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow I can't this morning, I made like 2 other mistakes similar, sorry about the noise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a weird diff!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These look great 👍🏻
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
CommandGroup
names 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.