Skip to content
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

Add API for titling an option group #492

Merged
merged 8 commits into from Sep 26, 2022
36 changes: 33 additions & 3 deletions Sources/ArgumentParser/Parsable Properties/OptionGroup.swift
Expand Up @@ -36,6 +36,9 @@ public struct OptionGroup<Value: ParsableArguments>: Decodable, ParsedWrapper {
// FIXME: Adding this property works around the crasher described in
// https://github.com/apple/swift-argument-parser/issues/338
internal var _dummy: Bool = false

/// The title to use in the help screen for this option group.
public var title: String = ""

internal init(_parsedValue: Parsed<Value>) {
self._parsedValue = _parsedValue
Expand All @@ -62,12 +65,27 @@ public struct OptionGroup<Value: ParsableArguments>: Decodable, ParsedWrapper {
}

/// Creates a property that represents another parsable type, using the
/// specified visibility.
public init(visibility: ArgumentVisibility = .default) {
/// specified title and visibility.
///
/// - Parameters:
/// - title: A title for grouping this option group's members in your
/// command's help screen. If `title` is empty, the members will be
/// displayed alongside the other arguments, flags, and options declared
/// by your command.
/// - visibility: The visibility to use for the entire option group.
public init(
title: String = "",
visibility: ArgumentVisibility = .default
) {
self.init(_parsedValue: .init { _ in
ArgumentSet(Value.self, visibility: .private)
var args = ArgumentSet(Value.self, visibility: .private)
args.content.withEach {
$0.help.parentTitle = title
}
return args
})
self._visibility = visibility
self.title = title
}

/// The value presented by this property wrapper.
Expand Down Expand Up @@ -111,3 +129,15 @@ extension OptionGroup {
self.init(visibility: .default)
}
}

// MARK: Deprecated

extension OptionGroup {
@_disfavoredOverload
@available(*, deprecated, renamed: "init(title:visibility:)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this still, SAP isn't ABI stable and the above initializer is source compatible?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, well very very minimally source breaking if someone ever used a point free version of the function: OptionGroup.init(visibility:)I really doubt this is the case though.

public init(
visibility _visibility: ArgumentVisibility = .default
) {
self.init(title: "", visibility: _visibility)
}
}
2 changes: 2 additions & 0 deletions Sources/ArgumentParser/Parsing/ArgumentDefinition.swift
Expand Up @@ -54,6 +54,7 @@ struct ArgumentDefinition {
var discussion: String
var valueName: String
var visibility: ArgumentVisibility
var parentTitle: String

init(
allValues: [String],
Expand All @@ -72,6 +73,7 @@ struct ArgumentDefinition {
self.discussion = help?.discussion ?? ""
self.valueName = help?.valueName ?? ""
self.visibility = help?.visibility ?? .default
self.parentTitle = ""
}
}

Expand Down
23 changes: 21 additions & 2 deletions Sources/ArgumentParser/Usage/HelpGenerator.swift
Expand Up @@ -51,6 +51,7 @@ internal struct HelpGenerator {
case positionalArguments
case subcommands
case options
case title(String)

var description: String {
switch self {
Expand All @@ -60,6 +61,8 @@ internal struct HelpGenerator {
return "Subcommands"
case .options:
return "Options"
case .title(let name):
return name
}
}
}
Expand Down Expand Up @@ -136,6 +139,8 @@ internal struct HelpGenerator {

var positionalElements: [Section.Element] = []
var optionElements: [Section.Element] = []
var titledSections: [String: [Section.Element]] = [:]
var sectionTitles: [String] = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed? It seems like a duplicate of titledSections.keys

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NVM this is so they are sorted. A comment about this would be nice!


/// Start with a full slice of the ArgumentSet so we can peel off one or
/// more elements at a time.
Expand Down Expand Up @@ -183,9 +188,15 @@ internal struct HelpGenerator {
}

let element = Section.Element(label: synopsis, abstract: description, discussion: arg.help.discussion)
if case .positional = arg.kind {
switch (arg.kind, arg.help.parentTitle) {
case (_, let sectionTitle) where !sectionTitle.isEmpty:
if !titledSections.keys.contains(sectionTitle) {
sectionTitles.append(sectionTitle)
}
titledSections[sectionTitle, default: []].append(element)
case (.positional, _):
positionalElements.append(element)
} else {
default:
optionElements.append(element)
}
}
Expand All @@ -203,8 +214,16 @@ internal struct HelpGenerator {
abstract: command.configuration.abstract)
}

// Combine the compiled groups in this order:
// - arguments
// - named sections
// - options/flags
// - subcommands
Comment on lines +219 to +223
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rauhul @ethan-kusters Does this seem like the correct order?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this makes sense to me!

return [
Section(header: .positionalArguments, elements: positionalElements),
] + sectionTitles.map { name in
Section(header: .title(name), elements: titledSections[name, default: []])
} + [
Section(header: .options, elements: optionElements),
Section(header: .subcommands, elements: subcommandElements),
]
Expand Down
10 changes: 10 additions & 0 deletions Sources/ArgumentParser/Utilities/CollectionExtensions.swift
Expand Up @@ -14,3 +14,13 @@ extension Collection {
isEmpty ? replacement() : self
}
}

extension MutableCollection {
mutating func withEach(_ body: (inout Element) throws -> Void) rethrows {
var i = startIndex
while i < endIndex {
try body(&self[i])
formIndex(after: &i)
}
}
}
1 change: 1 addition & 0 deletions Tests/ArgumentParserUnitTests/CMakeLists.txt
Expand Up @@ -4,6 +4,7 @@ add_library(UnitTests
HelpGenerationTests.swift
HelpGenerationTests+AtArgument.swift
HelpGenerationTests+AtOption.swift
HelpGenerationTests+GroupName.swift
NameSpecificationTests.swift
SplitArgumentTests.swift
StringSnakeCaseTests.swift
Expand Down